"""
Simple compiler that turns a Query object into SPARQL. Supports basic filters,
limit, and projecting declared fields.
"""

# Import for type hints - avoid circular import at runtime
import types
from dataclasses import dataclass
from typing import (
    TYPE_CHECKING,
    Annotated,
    Any,
    Generic,
    Iterator,
    TypeVar,
    Union,
    get_args,
    get_origin,
)

from rdflib import URIRef

from ..exc.exceptions import ConfigurationError, QueryError
from .hydrator import ResultHydrator
from .security import (
    DANGEROUS_IRI_KEYWORDS,
    SPARQLInjectionError,
    escape_sparql_iri,
    escape_sparql_literal,
    format_sparql_value_secure,
    validate_no_sparql_keywords,
    validate_sparql_operator,
)

# Forward references for type hints to avoid circular imports
if TYPE_CHECKING:
    from .filtering import FieldFilter, FilterExpression, FilterLike, LogicalExpression

if TYPE_CHECKING:
    from sparqlmojo.engine.session import Session
    from sparqlmojo.orm.model import Model

# Import field types for type checking at runtime
from sparqlmojo.orm.model import (
    COLLECTION_CONCAT_SUFFIX,
    DEFAULT_COLLECTION_SEPARATOR,
    IRIField,
    PropertyPath,
    is_collection_field,
)

# TypeVar for generic Query class
T = TypeVar("T", bound="Model")

# SPARQL variable name for subject (used in VALUES clause and elsewhere)
SUBJECT_VARIABLE_NAME: str = "s"


@dataclass(frozen=True)
class Condition:
    field_name: str
    op: str
    value: Any

    def _format_filter_value(
        self, field_name: str, value: Any, model_class: "type[T] | None" = None
    ) -> str:
        """Format a value for use in SPARQL FILTER clause.

        Args:
            field_name: Name of the field being filtered
            value: Value to format
            model_class: Optional model class for IRI field detection

        Returns:
            Formatted SPARQL value string
        """
        from .model import IRIField, LiteralField

        # Subject variable filters on IRIs, not literals
        # Format as IRI with angle brackets: <http://example.org/resource>
        if field_name == SUBJECT_VARIABLE_NAME:
            escaped_iri: str = escape_sparql_iri(str(value))
            return f"<{escaped_iri}>"

        # Check if field is an IRI-type field in the model
        # This covers IRIField, ObjectPropertyField, and SubjectField
        if model_class is not None:
            rdf_fields: dict[str, Any] = getattr(model_class, "_rdf_fields", {})
            if field_name in rdf_fields:
                field_info = rdf_fields[field_name]
                if isinstance(field_info, IRIField):
                    return field_info.format_value(value)
                if (
                    isinstance(field_info, LiteralField)
                    and field_info.datatype is not None
                ):
                    return field_info.format_value(value)

        # Values with to_sparql_filter() format themselves (e.g. LangLiteral).
        # TODO: duplicated in filtering.FieldFilter._format_value(); extract a shared
        # helper if a third call-site appears.
        if hasattr(value, "to_sparql_filter"):
            return str(value.to_sparql_filter())

        # Regular fields use secure value formatting for literals
        return format_sparql_value_secure(value)

    def to_sparql(self, model_class: "type[T] | None" = None) -> str:
        """Convert condition to SPARQL FILTER clause with injection protection.

        Args:
            model_class: Optional model class for IRI field detection

        Security:
        - Validates operators against a whitelist of safe operators
        - Uses secure value formatting that escapes string literals
        - Detects and rejects SPARQL keywords in user-provided values
        - Provides protection against SPARQL injection attacks
        """
        var: str = f"?{self.field_name}"

        # Validate operator for security
        safe_op: str = validate_sparql_operator(self.op)

        # Format value based on field type (pass model_class for IRI detection)
        val: str = self._format_filter_value(self.field_name, self.value, model_class)

        return f"FILTER({var} {safe_op} {val}) ."


class Query(Generic[T]):
    def __init__(self, model_cls: "type[T]", session: "Session | None" = None) -> None:
        self.model: type[T] = model_cls
        self._session: "Session | None" = session
        self._conditions: list["FilterLike"] = []
        self._limit: int | None = None
        self._offset: int | None = None
        self._from_graph: str | None = None
        self._from_named_graphs: list[str] = []
        self._values: dict[str, list[Any]] | None = None
        self._order_by_field: str | None = None
        self._order_descending: bool = False
        self._distinct: bool = False
        self._property_paths: dict[str, PropertyPath] = {}
        self._aggregates: dict[str, tuple[str, str]] = {}  # {alias: (func, field)}
        self._group_by_fields: list[str] = []  # Fields to group by
        self._lang_filters: dict[str, list[str]] = {}  # {field_name: [lang_codes]}
        # Track IRI field bindings from filter_by() for direct triple pattern binding
        # Instead of OPTIONAL + FILTER, these generate: ?s <predicate> <IRI> .
        self._filter_by_iri_bindings: dict[str, str] = {}  # {field_name: iri_value}
        # ResultHydrator for converting SPARQL JSON to model instances
        self._hydrator: ResultHydrator[T] = ResultHydrator(model_cls, session)

    def _validate_field_exists(self, field: str) -> None:
        """Validate that a field exists in the model.

        Args:
            field: Field name to validate

        Raises:
            QueryError: If field doesn't exist in model
        """
        if field not in self.model._rdf_fields:
            available_fields: list[str] = list(self.model._rdf_fields.keys())
            raise QueryError(
                f"Field '{field}' not found in model. "
                f"Available fields: {available_fields}"
            )

    def filter(
        self, *conditions: Union[Condition, "FilterExpression", "LogicalExpression"]
    ) -> "Query[T]":
        """
        Add filter conditions to the query.

        Note: Using Union instead of | operator because this involves forward references
        which are not supported by the | operator in Python's type system.

        Note: Unlike filter_by(), this method always generates FILTER clauses,
        even for IRIField. Use filter_by() for IRI fields when you want direct
        triple pattern binding (which provides better query semantics and
        ensures non-existent IRIs return no results).

        Args:
            *conditions: One or more filter conditions (Condition,
                FilterExpression, or LogicalExpression)

        Returns:
            Self for method chaining

        Example:
            >>> # Old style with Condition
            >>> query.filter(Condition("name", "=", "Alice"))
            >>>
            >>> # New style with field-level filtering
            >>> query.filter(Person.name == "Alice")
            >>> query.filter(Person.age > 18, Person.name.contains("Smith"))
        """
        # Import here to avoid circular imports

        for condition in conditions:
            self._conditions.append(condition)
        return self

    def _handle_filter_by_field(
        self, field_name: str, value: Any, field_info: Any
    ) -> None:
        """Handle filter_by() for a single field based on its type.

        This method implements a field-type-based dispatch pattern:
        - SubjectField: Uses VALUES clause via Condition (handled by
          _extract_subject_conditions)
        - IRIField (including ObjectPropertyField): Uses direct triple pattern
          binding for correct query semantics
        - LiteralField: Uses FILTER clause for equality comparison

        Args:
            field_name: Name of the field being filtered
            value: The value to filter by
            field_info: RDF field metadata from model._rdf_fields
        """
        # Import IRIField here to avoid circular imports at module level
        from .model import IRIField

        if field_info.is_subject():
            # SubjectField uses "s" as the variable name (subject variable)
            # and uses VALUES clause (handled by _extract_subject_conditions)
            condition: Condition = Condition("s", "=", value)
            self._conditions.append(condition)
        elif isinstance(field_info, IRIField):
            # IRIField (and subclasses like ObjectPropertyField):
            # Store binding for direct triple pattern generation
            # This avoids OPTIONAL + FILTER which causes incorrect results
            self._filter_by_iri_bindings[field_name] = str(value)
        else:
            # Literal fields use FILTER as before
            condition = Condition(field_name, "=", value)
            self._conditions.append(condition)

    def filter_by(self, **kwargs: Any) -> "Query[T]":
        """
        SQLAlchemy-style filtering using keyword arguments.

        For IRIField and its subclasses (ObjectPropertyField), this generates
        direct triple pattern bindings instead of OPTIONAL + FILTER patterns:
            ?s <predicate> <IRI> .
            BIND(<IRI> AS ?field) .

        This ensures correct query semantics where non-existent IRIs return
        no results, rather than incorrectly matching unrelated subjects.

        Note: Unlike filter(), which always generates FILTER clauses, filter_by()
        uses field-type-aware dispatching:
        - SubjectField: VALUES clause for efficient subject binding
        - IRIField/ObjectPropertyField: Direct triple pattern binding
        - LiteralField: FILTER clause for equality comparison

        Args:
            **kwargs: Field name to value mappings for equality filters

        Returns:
            Self for method chaining

        Example:
            >>> session.query(Person).filter_by(name="Alice", age=30)
            >>> # Filter by subject IRI using SubjectField
            >>> session.query(Label).filter_by(entity_iri="http://example.org/entity")
            >>> # Filter by IRI field (generates direct triple pattern)
            >>> session.query(Translation).filter_by(work_iri="http://example.org/work")
        """
        for field_name, value in kwargs.items():
            # Validate field exists in model
            if field_name not in self.model._rdf_fields:
                available_fields: list[str] = list(self.model._rdf_fields.keys())
                raise QueryError(
                    f"Field '{field_name}' not found in model. "
                    f"Available fields: {available_fields}"
                )

            field_info = self.model._rdf_fields[field_name]
            self._handle_filter_by_field(field_name, value, field_info)
        return self

    def filter_lang(self, field_name: str, langs: str | list[str]) -> "Query[T]":
        """
        Filter a LangString field by language tag(s).

        Args:
            field_name: Name of the LangString field to filter
            langs: Single language code or list of language codes (e.g., 'en',
                   ['en', 'fr'])

        Returns:
            Self for method chaining

        Raises:
            QueryError: If field does not exist or is not a LangString field

        Example:
            >>> # Filter to only English labels
            >>> session.query(Label).filter_lang("text", "en")
            >>>
            >>> # Filter to English or French labels
            >>> session.query(Label).filter_lang("text", ["en", "fr"])
        """
        # Validate field exists
        if field_name not in self.model._rdf_fields:
            available_fields: list[str] = list(self.model._rdf_fields.keys())
            raise QueryError(
                f"Field '{field_name}' not found in model. "
                f"Available fields: {available_fields}"
            )

        # Validate field supports language filtering
        field_info = self.model._rdf_fields[field_name]
        if not field_info.supports_language_filtering():
            raise QueryError(
                f"Field '{field_name}' does not support language filtering. "
                f"filter_lang() can only be used with LangString fields."
            )

        # Normalize langs to a list
        lang_list: list[str] = [langs] if isinstance(langs, str) else list(langs)

        # Store the language filter
        self._lang_filters[field_name] = lang_list

        return self

    def order_by(self, field: str, descending: bool = False) -> "Query[T]":
        """
        Add ORDER BY clause to query.

        Args:
            field: Field name to order by
            descending: If True, order descending (default: False)

        Returns:
            Self for method chaining

        Example:
            >>> session.query(Person).order_by("age", descending=True)
        """
        self._validate_field_exists(field)
        self._order_by_field = field
        self._order_descending = descending
        return self

    def distinct(self) -> "Query[T]":
        """
        Add DISTINCT modifier to query.

        Returns:
            Self for method chaining

        Example:
            >>> session.query(Person).distinct()
        """
        self._distinct = True
        return self

    def count_(self, field: str = "*", alias: str = "count") -> "Query[T]":
        """
        Add COUNT aggregate to query.

        Args:
            field: Field name to count, or "*" for all results
            alias: Alias for the aggregate result variable

        Returns:
            Self for method chaining

        Example:
            >>> session.query(Person).count_().first()
            >>> session.query(Person).count_('age', 'age_count').first()
        """
        if field != "*":
            self._validate_field_exists(field)
        self._aggregates[alias] = ("COUNT", field)
        return self

    def sum(self, field: str, alias: str = "sum") -> "Query[T]":
        """
        Add SUM aggregate to query.

        Args:
            field: Field name to sum
            alias: Alias for the aggregate result variable

        Returns:
            Self for method chaining

        Example:
            >>> session.query(Person).sum('age').first()
        """
        self._validate_field_exists(field)
        self._aggregates[alias] = ("SUM", field)
        return self

    def avg(self, field: str, alias: str = "avg") -> "Query[T]":
        """
        Add AVG aggregate to query.

        Args:
            field: Field name to average
            alias: Alias for the aggregate result variable

        Returns:
            Self for method chaining

        Example:
            >>> session.query(Person).avg('age').first()
        """
        self._validate_field_exists(field)
        self._aggregates[alias] = ("AVG", field)
        return self

    def min(self, field: str, alias: str = "min") -> "Query[T]":
        """
        Add MIN aggregate to query.

        Args:
            field: Field name to find minimum
            alias: Alias for the aggregate result variable

        Returns:
            Self for method chaining

        Example:
            >>> session.query(Person).min('age').first()
        """
        self._validate_field_exists(field)
        self._aggregates[alias] = ("MIN", field)
        return self

    def max(self, field: str, alias: str = "max") -> "Query[T]":
        """
        Add MAX aggregate to query.

        Args:
            field: Field name to find maximum
            alias: Alias for the aggregate result variable

        Returns:
            Self for method chaining

        Example:
            >>> session.query(Person).max('age').first()
        """
        self._validate_field_exists(field)
        self._aggregates[alias] = ("MAX", field)
        return self

    def group_by(self, *fields: str) -> "Query[T]":
        """
        Add GROUP BY clause to query.

        Args:
            *fields: Field names to group by

        Returns:
            Self for method chaining

        Example:
            >>> session.query(Person).group_by('age').count_().all()
        """
        for field in fields:
            self._validate_field_exists(field)
        self._group_by_fields.extend(fields)
        return self

    def limit(self, n: int) -> "Query[T]":
        self._limit = n
        return self

    def offset(self, n: int) -> "Query[T]":
        self._offset = n
        return self

    def from_graph(self, graph_uri: str) -> "Query[T]":
        """Specify the default graph for this query.

        Args:
            graph_uri: URI of the default graph to query

        Returns:
            Self for method chaining
        """
        self._from_graph = graph_uri
        return self

    def from_named(self, graph_uri: str) -> "Query[T]":
        """Add a named graph to query from.

        Args:
            graph_uri: URI of the named graph to query

        Returns:
            Self for method chaining
        """
        if graph_uri not in self._from_named_graphs:
            self._from_named_graphs.append(graph_uri)
        return self

    def clear_graphs(self) -> "Query[T]":
        """Clear all graph specifications for this query.

        Returns:
            Self for method chaining
        """
        self._from_graph = None
        self._from_named_graphs = []
        return self

    def values(
        self,
        field_or_bindings: "FieldFilter | dict[str, list[Any]]",
        values_list: list[Any] | None = None,
        *,
        validate_against_model: bool = True,
    ) -> "Query[T]":
        """Add VALUES clause to constrain query with explicit value sets.

        Supports two calling conventions:

        ORM-style (recommended):
            query.values(Model.field, [value1, value2])

            The ORM-style API automatically:
            - Maps SubjectField to ?s variable
            - Formats IRIs with angle brackets for SubjectField/IRIField
            - Formats literals with quotes for LiteralField
            - Performs type validation based on field annotations

        Dict-style (for multiple fields):
            query.values({'name': ['Alice', 'Bob'], 'age': [25, 30]})

        For SubjectField, the ORM-style automatically maps to the correct
        SPARQL variable (?s), eliminating the need to use raw variable names.

        Args:
            field_or_bindings: Either a FieldFilter (Model.field) for ORM-style,
                             or a dict mapping field names to value lists.
            values_list: List of values when using ORM-style (required if
                        field_or_bindings is a FieldFilter).
            validate_against_model: If True (default), validates that variable
                                  names match model fields and types are
                                  compatible. Set to False for raw variable
                                  names like 's'.

        Returns:
            Self for method chaining

        Raises:
            QueryError: If validation fails (empty bindings, inconsistent list
                      lengths, invalid field names, SPARQL injection attempts,
                      or type mismatches)

        Examples:
            >>> # ORM-style - type-safe, model-aware
            >>> query = session.query(Label).values(Label.entity_iri, [uri1, uri2])
            >>> # Generates: VALUES (?s) { (<uri1>) (<uri2>) }
            >>>
            >>> # Dict-style - for multiple fields
            >>> query = session.query(Person).values({
            ...     'name': ['Bob', 'Alice'],
            ...     'age': [30, 25]
            ... })
            >>> # Generates: VALUES (?name ?age) { ("Bob" 30) ("Alice" 25) }
        """
        # Convert input to normalized bindings format
        bindings, is_orm_subject_field = self._normalize_values_input(
            field_or_bindings, values_list
        )

        # Validate bindings structure and variable names
        self._validate_values_bindings(bindings)

        # Validate values for security (injection prevention)
        self._validate_values_security(bindings)

        # Validate against model if requested
        # Skip validation for ORM-style SubjectField (already type-safe)
        # and for 's' variable which won't be in model fields
        if validate_against_model and self.model and not is_orm_subject_field:
            fields_to_validate: dict[str, list[Any]] = {
                k: v for k, v in bindings.items() if k != SUBJECT_VARIABLE_NAME
            }
            if fields_to_validate:
                self._validate_values_against_model(fields_to_validate)

        self._values = bindings
        return self

    def _normalize_values_input(
        self,
        field_or_bindings: "FieldFilter | dict[str, list[Any]]",
        values_list: list[Any] | None,
    ) -> tuple[dict[str, list[Any]], bool]:
        """Convert ORM-style or dict-style input to normalized bindings.

        SubjectField names in dict-style input are automatically mapped to
        the 's' variable for consistency with ORM-style API (Issue #129).

        Args:
            field_or_bindings: Either a FieldFilter (Model.field) for ORM-style,
                             or a dict mapping field names to value lists.
            values_list: List of values when using ORM-style.

        Returns:
            Tuple of (bindings dict, is_orm_subject_field flag). The flag is
            True when a SubjectField was referenced by its model field name
            (not when 's' is used directly).

        Raises:
            QueryError: If ORM-style is used without providing values_list.
        """
        from .filtering import FieldFilter

        is_orm_subject_field: bool = False

        if isinstance(field_or_bindings, FieldFilter):
            if values_list is None:
                raise QueryError(
                    "values() requires a list of values when using ORM-style syntax: "
                    "query.values(Model.field, [value1, value2])"
                )

            field_name: str = field_or_bindings.field_name
            var_name: str = self._get_sparql_variable_for_field(field_name)

            if self._is_orm_subject_field_mapping(field_name, var_name):
                is_orm_subject_field = True

            bindings: dict[str, list[Any]] = {var_name: values_list}
        else:
            # Dict-style input - map SubjectField names to "s" for consistency
            # with ORM-style API (Issue #129)
            bindings = {}
            for var_name, values in field_or_bindings.items():
                mapped_name: str = self._get_sparql_variable_for_field(var_name)
                if self._is_orm_subject_field_mapping(var_name, mapped_name):
                    is_orm_subject_field = True
                # Detect conflicting mappings to the same variable (e.g., both
                # "s" and "entity_iri" when entity_iri is a SubjectField)
                if mapped_name in bindings:
                    raise QueryError(
                        f"Conflicting bindings for variable '{mapped_name}': "
                        f"both '{var_name}' and another key map to the same "
                        f"SPARQL variable. Use only one of them."
                    )
                bindings[mapped_name] = values

        return bindings, is_orm_subject_field

    def _is_orm_subject_field_mapping(
        self, original_name: str, mapped_name: str
    ) -> bool:
        """Check if field is an ORM SubjectField mapped to 's' variable.

        Args:
            original_name: Original field name from user input.
            mapped_name: SPARQL variable name after mapping.

        Returns:
            True if this is a SubjectField mapped from a model field name
            (not when 's' is used directly).
        """
        return (
            mapped_name == SUBJECT_VARIABLE_NAME
            and original_name != SUBJECT_VARIABLE_NAME
        )

    def _get_sparql_variable_for_field(self, field_name: str) -> str:
        """Get SPARQL variable name for a model field.

        SubjectField maps to 's', other fields use their field name.

        Args:
            field_name: The name of the field in the model.

        Returns:
            SPARQL variable name (without ? prefix).
        """
        if self.model:
            rdf_fields: dict[str, Any] = getattr(self.model, "_rdf_fields", {})
            if field_name in rdf_fields:
                field_info = rdf_fields[field_name]
                if getattr(field_info, "is_subject_field", False):
                    return SUBJECT_VARIABLE_NAME
        return field_name

    def _validate_values_bindings(self, bindings: dict[str, list[Any]]) -> None:
        """Validate bindings structure and variable names.

        Args:
            bindings: Dictionary mapping variable names to lists of values.

        Raises:
            QueryError: If bindings are empty, lists have inconsistent lengths,
                      or variable names are invalid.
        """
        if not bindings:
            raise QueryError("VALUES clause requires at least one variable binding")

        # Check no value lists are empty
        for var_name, vals in bindings.items():
            if len(vals) == 0:
                raise QueryError(
                    f"VALUES clause for variable '{var_name}' has an empty "
                    f"value list. Provide at least one value."
                )

        # Check all lists have same length
        list_lengths: list[int] = [len(vals) for vals in bindings.values()]
        if len(set(list_lengths)) != 1:
            raise QueryError(
                f"VALUES clause requires all value lists to have the same length. "
                f"Got lengths: {list_lengths}"
            )

        # Check for empty or whitespace-only string values
        for var_name, vals in bindings.items():
            for i, val in enumerate(vals):
                if isinstance(val, str) and (not val or not val.strip()):
                    raise QueryError(
                        f"VALUES clause for variable '{var_name}' contains an "
                        f"empty or whitespace-only value at index {i}. "
                        f"All values must be non-empty."
                    )

        # Validate each variable name
        for var_name in bindings.keys():
            self._validate_sparql_variable_name(var_name)

    def _validate_sparql_variable_name(self, var_name: str) -> None:
        """Validate a SPARQL variable name.

        Args:
            var_name: The variable name to validate.

        Raises:
            QueryError: If variable name is empty, contains dangerous SPARQL
                      keywords, or is not a valid identifier.
        """
        if not var_name or not var_name.strip():
            raise QueryError("Variable names cannot be empty")

        # Check for dangerous SPARQL keywords using word boundary matching
        # from security.py to avoid false positives
        try:
            validate_no_sparql_keywords(
                var_name, DANGEROUS_IRI_KEYWORDS, "Variable name"
            )
        except SPARQLInjectionError as e:
            raise QueryError(
                f"Invalid variable name: '{var_name}' contains SPARQL keywords"
            ) from e

        # Validate variable name format
        if not var_name.replace(" ", "").isidentifier():
            raise QueryError(
                f"Invalid variable name: '{var_name}'. Must be a valid Python "
                "identifier"
            )

    def _validate_values_security(self, bindings: dict[str, list[Any]]) -> None:
        """Check for SPARQL injection in values.

        Args:
            bindings: Dictionary mapping variable names to lists of values.

        Raises:
            QueryError: If any value contains dangerous SPARQL patterns.
        """
        from .security import escape_sparql_literal

        for var_name, vals in bindings.items():
            for value in vals:
                if isinstance(value, str):
                    try:
                        escape_sparql_literal(value)
                    except Exception as e:
                        raise QueryError(
                            f"Invalid value for variable '{var_name}': {e}"
                        )

    def path(self, field: str, path: PropertyPath) -> "Query[T]":
        """
        Use property path for field instead of simple predicate.

        Property paths allow traversing multiple relationship levels and using
        path operators like zero-or-more (*), one-or-more (+), zero-or-one (?),
        alternative paths (|), and inverse paths (^).

        Args:
            field: Field name to apply property path to
            path: PropertyPath object defining the path

        Returns:
            Self for method chaining

        Raises:
            QueryError: If field doesn't exist in model

        Examples:
            >>> # Find all transitive knows relationships
            >>> session.query(Person).path(
            ...     'knows',
            ...     PropertyPath('schema:knows+')
            ... ).all()

            >>> # Find managers (boss or boss's boss, etc.)
            >>> session.query(Person).path(
            ...     'manager',
            ...     PropertyPath('org:reportsTo*')
            ... ).all()

            >>> # Find parents or guardians
            >>> session.query(Person).path(
            ...     'parent',
            ...     PropertyPath('schema:parent|schema:guardian')
            ... ).all()

            >>> # Find children (inverse path)
            >>> session.query(Person).path(
            ...     'child',
            ...     PropertyPath('^schema:child')
            ... ).all()
        """
        self._validate_field_exists(field)

        # Store property path mapping
        self._property_paths[field] = path
        return self

    def transitive(self, field: str) -> "Query[T]":
        """
        Apply transitive (one-or-more) property path operator to a field.

        This is a convenience method equivalent to:
            .path(field, PropertyPath(predicate+))
        where predicate is inferred from the field's definition.

        Args:
            field: Field name to apply transitive operator to

        Returns:
            Self for method chaining

        Raises:
            QueryError: If field doesn't exist in model

        Examples:
            >>> # Find all transitive "knows" relationships
            >>> session.query(Person).transitive('knows').all()

            >>> # Find people Alice knows (directly or indirectly)
            >>> session.query(Person).transitive('knows').filter_by(name='Alice')
        """
        self._validate_field_exists(field)

        # Get field's predicate and format for property path
        field_info = self.model._rdf_fields[field]
        formatted_pred: str = PropertyPath.format_predicate(field_info.predicate)

        # Create property path with + operator
        path: PropertyPath = PropertyPath(f"{formatted_pred}+")
        self._property_paths[field] = path
        return self

    def zero_or_more(self, field: str) -> "Query[T]":
        """
        Apply zero-or-more property path operator to a field.

        This is a convenience method equivalent to:
            .path(field, PropertyPath(predicate*))
        where predicate is inferred from the field's definition.

        Args:
            field: Field name to apply zero-or-more operator to

        Returns:
            Self for method chaining

        Raises:
            QueryError: If field doesn't exist in model

        Examples:
            >>> # Find all managers (direct and indirect)
            >>> session.query(Person).zero_or_more('manager').all()

            >>> # Find management chain for a person
            >>> query = session.query(Person).zero_or_more('manager')
            >>> query = query.filter_by(name='Alice')
        """
        self._validate_field_exists(field)

        # Get field's predicate and format for property path
        field_info = self.model._rdf_fields[field]
        formatted_pred: str = PropertyPath.format_predicate(field_info.predicate)

        # Create property path with * operator
        path: PropertyPath = PropertyPath(f"{formatted_pred}*")
        self._property_paths[field] = path
        return self

    def zero_or_one(self, field: str) -> "Query[T]":
        """
        Apply zero-or-one property path operator to a field.

        This is a convenience method equivalent to:
            .path(field, PropertyPath(predicate?))
        where predicate is inferred from the field's definition.

        Args:
            field: Field name to apply zero-or-one operator to

        Returns:
            Self for method chaining

        Raises:
            QueryError: If field doesn't exist in model

        Examples:
            >>> # Find people who may or may not have a parent
            >>> session.query(Person).zero_or_one('parent').all()
        """
        self._validate_field_exists(field)

        # Get field's predicate and format for property path
        field_info = self.model._rdf_fields[field]
        formatted_pred: str = PropertyPath.format_predicate(field_info.predicate)

        # Create property path with ? operator
        path: PropertyPath = PropertyPath(f"{formatted_pred}?")
        self._property_paths[field] = path
        return self

    def alternative(self, field: str, *alternative_fields: str) -> "Query[T]":
        """
        Apply alternative paths operator to multiple fields.

        This is a convenience method that creates a property path using the | operator
        for the predicates of the specified fields.

        Args:
            field: Primary field name
            *alternative_fields: Additional field names for alternatives

        Returns:
            Self for method chaining

        Raises:
            QueryError: If any field doesn't exist in model

        Examples:
            >>> # Find people who have either a parent or guardian
            >>> session.query(Person).alternative('parent', 'guardian').all()

            >>> # Find contact via email or phone
            >>> session.query(Person).alternative('email', 'phone', 'fax').all()
        """
        # Validate all fields exist
        all_fields: list[str] = [field] + list(alternative_fields)
        for f in all_fields:
            self._validate_field_exists(f)

        # Collect predicates and format for property path
        predicates: list[str] = []
        for f in all_fields:
            field_info = self.model._rdf_fields[f]
            predicates.append(PropertyPath.format_predicate(field_info.predicate))

        # Create property path with | operator
        path_str: str = "|".join(predicates)
        path: PropertyPath = PropertyPath(path_str)
        self._property_paths[field] = path
        return self

    def inverse(self, field: str) -> "Query[T]":
        """
        Apply inverse property path operator to a field.

        This is a convenience method equivalent to:
            .path(field, PropertyPath(^predicate))
        where predicate is inferred from the field's definition.

        Args:
            field: Field name to apply inverse operator to

        Returns:
            Self for method chaining

        Raises:
            QueryError: If field doesn't exist in model

        Examples:
            >>> # Find children (inverse of parent relationship)
            >>> session.query(Person).inverse('parent').all()

            >>> # Find subordinates (inverse of manager relationship)
            >>> session.query(Person).inverse('manager').all()
        """
        self._validate_field_exists(field)

        # Get field's predicate and format for property path
        field_info = self.model._rdf_fields[field]
        formatted_pred: str = PropertyPath.format_predicate(field_info.predicate)

        # Create property path with ^ operator
        path: PropertyPath = PropertyPath(f"^{formatted_pred}")
        self._property_paths[field] = path
        return self

    def _validate_values_against_model(self, bindings: dict[str, list[Any]]) -> None:
        """Validate VALUES bindings against the model's field definitions.

        Args:
            bindings: Dictionary mapping variable names to lists of values.

        Raises:
            QueryError: If variable names don't match model fields or
                      value types are incompatible with field types.
        """
        if not self.model:
            return

        # Get model field information
        model_fields = getattr(self.model, "_rdf_fields", {})
        model_annotations = getattr(self.model, "__annotations__", {})

        # Check that all variable names correspond to model fields
        for var_name in bindings.keys():
            if var_name not in model_fields:
                available_fields = list(model_fields.keys())
                raise QueryError(
                    f"Variable '{var_name}' does not correspond to a model field. "
                    f"Available fields: {available_fields}"
                )

            # Get the expected type for this field
            if var_name in model_annotations:
                expected_type = model_annotations[var_name]

                # Handle Annotated types by extracting the base type
                if get_origin(expected_type) is Annotated:
                    # Get the first argument (the actual type)
                    type_args = get_args(expected_type)
                    if type_args:
                        expected_type = type_args[0]

                # Validate that all values match the expected type
                for value in bindings[var_name]:
                    self._validate_value_type(value, expected_type, var_name)

    def _validate_value_type(
        self, value: Any, expected_type: type, field_name: str
    ) -> None:
        """Validate that a value matches the expected type.

        Args:
            value: The value to validate
            expected_type: The expected type
            field_name: Name of the field for error messages

        Raises:
            QueryError: If the value type is incompatible with the expected type
        """
        # Check for both Union (typing.Union) and UnionType (X | Y syntax)
        origin = get_origin(expected_type)
        if origin is Union or isinstance(expected_type, types.UnionType):
            self._validate_union_type(value, expected_type, field_name)
        else:
            self._validate_simple_type(value, expected_type, field_name)

    def _validate_union_type(
        self, value: Any, union_type: type, field_name: str
    ) -> None:
        """Validate value against Union type (including Optional).

        Args:
            value: The value to validate
            union_type: Union type annotation
            field_name: Name of the field for error messages

        Raises:
            QueryError: If value doesn't match any type in the union
        """
        union_types = get_args(union_type)

        # Handle None values
        if value is None:
            if type(None) not in union_types:
                raise QueryError(
                    f"None not allowed for field '{field_name}'. "
                    f"Expected type {union_type}"
                )
            return

        # Check non-None types
        non_none_types = [t for t in union_types if t is not type(None)]
        for expected in non_none_types:
            if self._value_matches_type(value, expected):
                return

        raise QueryError(
            f"Value '{value}' for field '{field_name}' does not match "
            f"expected type {union_type}"
        )

    def _validate_simple_type(
        self, value: Any, expected_type: type, field_name: str
    ) -> None:
        """Validate value against simple (non-Union) type.

        Args:
            value: The value to validate
            expected_type: The expected type
            field_name: Name of the field for error messages

        Raises:
            QueryError: If value doesn't match the expected type
        """
        if not self._value_matches_type(value, expected_type):
            raise QueryError(
                f"Value '{value}' for field '{field_name}' does not match "
                f"expected type {expected_type}"
            )

    def _value_matches_type(self, value: Any, expected_type: type) -> bool:
        """Check if a value matches the expected type."""
        # Handle None values
        if value is None:
            return True  # None is generally acceptable for optional fields

        # Handle basic types
        if expected_type in (str, int, float, bool):
            return isinstance(value, expected_type)

        # Handle more complex cases could be added here
        # For now, be permissive for other types
        return True

    def _format_values_clause_value(self, var_name: str, value: Any) -> str:
        """
        Format a single value for use in VALUES clause.

        Detects whether value should be formatted as IRI or literal based on:
        1. Field type lookup in model (IRIField and its subclasses)
        2. LiteralField with explicit datatype
        3. rdflib.URIRef instance detection
        4. Variable name heuristics (e.g., 's' for subject)

        Args:
            var_name: The SPARQL variable name (without ?)
            value: The value to format

        Returns:
            Formatted SPARQL value string with proper escaping
        """
        # Import field types for isinstance checks
        from .model import IRIField, LiteralField

        # Handle None as UNDEF in SPARQL
        if value is None:
            return "UNDEF"

        # Detect rdflib.URIRef instances
        if isinstance(value, URIRef):
            # URIRef instances are IRIs
            escaped_iri: str = escape_sparql_iri(str(value))
            return f"<{escaped_iri}>"

        # Check if variable corresponds to an IRI field in the model
        # This covers IRIField, ObjectPropertyField, and SubjectField
        if self.model and hasattr(self.model, "_rdf_fields"):
            rdf_fields: dict[str, Any] = self.model._rdf_fields

            if var_name in rdf_fields:
                field_info = rdf_fields[var_name]

                # Check if this is an IRI-type field
                if isinstance(field_info, IRIField):
                    # Use the field's polymorphic format_value method
                    return field_info.format_value(value)

                # Check if this is a LiteralField with explicit datatype
                if (
                    isinstance(field_info, LiteralField)
                    and field_info.datatype is not None
                ):
                    return field_info.format_value(value)

        # Heuristic: subject variable typically represents the subject (IRI)
        if (
            var_name == SUBJECT_VARIABLE_NAME
            and isinstance(value, str)
            and value.strip()
        ):
            escaped_subject_iri: str = escape_sparql_iri(value)
            return f"<{escaped_subject_iri}>"

        # Default: format as literal using secure formatting
        return format_sparql_value_secure(value)

    def _add_subject_field_bind(
        self, where_lines: list[str], s_var: str = "?s"
    ) -> None:
        """Add BIND clause for SubjectField if present."""
        if self.model._subject_field_name:
            where_lines.append(f"BIND({s_var} AS ?{self.model._subject_field_name}) .")

    def _get_regular_fields(self) -> dict[str, Any]:
        """Get all RDF fields excluding SubjectField."""
        return {
            name: field
            for name, field in self.model._rdf_fields.items()
            if not field.is_subject()
        }

    def _has_collection_fields(self) -> bool:
        """Check if the model has any collection fields."""
        return any(
            is_collection_field(field) for field in self.model._rdf_fields.values()
        )

    def _get_collection_fields(self) -> dict[str, Any]:
        """Get all collection fields (LiteralList, LangStringList, IRIList)."""
        return {
            name: field
            for name, field in self.model._rdf_fields.items()
            if is_collection_field(field)
        }

    def _get_non_collection_regular_fields(self) -> dict[str, Any]:
        """Get regular fields that are not collection fields or SubjectField."""
        return {
            name: field
            for name, field in self.model._rdf_fields.items()
            if (not is_collection_field(field) and not field.is_subject())
        }

    def _escape_sparql_string(self, value: str) -> str:
        """
        Escape a string value for use in SPARQL query.

        Handles backslashes and double quotes which have special meaning in
        SPARQL string literals.

        Args:
            value: String value to escape

        Returns:
            Escaped string safe for use in SPARQL literals
        """
        return value.replace("\\", "\\\\").replace('"', '\\"')

    def _compile_collection_subquery_pattern(
        self,
        field_name: str,
        field_info: Any,  # CollectionFieldMixin subclass with predicate attribute
        s_var: str,
        subject_values_clause: str = "",
        graph_uris: list[str] | None = None,
    ) -> str:
        """
        Generate OPTIONAL subquery pattern for a collection field in WHERE clause.

        Uses OPTIONAL with a subquery that does GROUP BY internally. This avoids
        cartesian product explosion because each subquery returns at most one row
        per subject (already aggregated).

        When subject_values_clause is provided, it is inserted at the start of
        the subquery WHERE block so the endpoint can constrain the scan to only
        the specified subjects. Without this, large endpoints scan the entire
        dataset inside the subquery before joining.

        When graph_uris is provided, the inner triple pattern is wrapped in
        GRAPH clauses so the subquery is scoped to the same named graphs as
        the outer query. SPARQL 1.1 subqueries do not inherit FROM clauses
        from the outer query (SubSelect has no DatasetClause), so FROM in a
        subquery is not valid SPARQL 1.1. GRAPH patterns are the correct way
        to restrict a subquery to a specific named graph.

        Standard SPARQL 1.1 pattern with graph scoping:
            OPTIONAL {
                SELECT ?s (GROUP_CONCAT(?val; separator="...") AS ?field_concat)
                WHERE { VALUES (?s) { ... } GRAPH <graph> { ?s <predicate> ?val } }
                GROUP BY ?s
            }

        Args:
            field_name: Name of the collection field
            field_info: RDF field metadata (must be a collection field type)
            s_var: Subject variable (e.g., "?s")
            subject_values_clause: Optional VALUES clause to push into the
                subquery WHERE block for constraint pushdown
            graph_uris: Optional list of named graph URIs to scope the subquery
                triple pattern via GRAPH clauses. Multiple URIs are UNIONed.

        Returns:
            OPTIONAL subquery pattern string for WHERE clause
        """
        # Determine predicate expression: use property path if defined,
        # otherwise wrap the raw predicate in angle brackets
        if field_name in self._property_paths:
            # Query-level property path (from .path() method)
            predicate_expr: str = self._property_paths[field_name].to_sparql()
        elif field_info.is_property_path():
            # Field-level property path (defined in model field)
            # The predicate already contains the SPARQL property path expression
            predicate_expr = field_info.predicate
        else:
            predicate: str = field_info.predicate
            if self._session:
                predicate = self._session.expand_iri(predicate)
            if PropertyPath.has_operators(predicate):
                # Predicate contains path operators (e.g. "^<http://...>"),
                # use as-is to avoid double-wrapping
                predicate_expr = predicate
            else:
                predicate_expr = f"<{escape_sparql_iri(predicate)}>"

        sep: str = getattr(field_info, "separator", DEFAULT_COLLECTION_SEPARATOR)
        escaped_sep: str = self._escape_sparql_string(sep)
        inner_var: str = f"{field_name}_inner"
        concat_var: str = f"{field_name}{COLLECTION_CONCAT_SUFFIX}"
        limit: int | None = getattr(field_info, "limit", None)

        # Build the subquery SELECT clause using polymorphic method
        # Each collection field type knows how to generate its own GROUP_CONCAT
        group_concat_expr: str = field_info.get_group_concat_expr(
            inner_var, concat_var, escaped_sep
        )
        select_clause: str = f"{s_var} {group_concat_expr}"

        # Build the triple pattern (with optional VALUES pushdown)
        triple_pattern: str = ""
        if subject_values_clause:
            triple_pattern += f"{subject_values_clause} "
        triple_pattern += f"{s_var} {predicate_expr} ?{inner_var}"

        # Scope the triple pattern to named graphs using GRAPH clauses.
        # FROM in a SPARQL 1.1 subquery is not valid (SubSelect has no
        # DatasetClause), so we wrap the triple pattern in GRAPH instead.
        # Multiple graph URIs are UNIONed so values from each are aggregated.
        inner_where: str
        if graph_uris:
            if len(graph_uris) == 1:
                inner_where = (
                    f"GRAPH <{escape_sparql_iri(graph_uris[0])}> {{ {triple_pattern} }}"
                )
            else:
                inner_where = " UNION ".join(
                    f"{{ GRAPH <{escape_sparql_iri(uri)}> {{ {triple_pattern} }} }}"
                    for uri in graph_uris
                )
        else:
            inner_where = triple_pattern

        # Wrap with nested SELECT if limit is specified
        # This ensures LIMIT is applied before GROUP_CONCAT aggregation
        if limit is not None:
            inner_where = (
                f"{{ SELECT {s_var} ?{inner_var} "
                f"WHERE {{ {inner_where} }} "
                f"LIMIT {limit} }}"
            )

        return (
            f"OPTIONAL {{ SELECT {select_clause} "
            f"WHERE {{ {inner_where} }} "
            f"GROUP BY {s_var} }}"
        )

    def _extract_subject_conditions(self) -> tuple[list[str], list["FilterLike"]]:
        """
        Extract subject equality conditions for VALUES clause generation.

        When filter_by() is used on a SubjectField, it creates
        Condition("s", "=", value). These conditions should be converted to
        VALUES clauses instead of FILTER clauses for efficient query execution
        on large endpoints.

        Only equality conditions (op == "=") are extracted for VALUES clause
        generation. Non-equality operators (>, <, >=, <=, !=) must remain as
        FILTER clauses because:
        1. VALUES clauses only support exact value matching, not comparisons
        2. Non-equality filters require the SPARQL engine to evaluate each
           candidate against the condition, which inherently requires scanning

        LogicalExpression objects (AND/OR combinations) are also kept as FILTER
        because they may contain complex nested conditions that cannot be
        expressed in a simple VALUES clause.

        Returns:
            Tuple of (subject_values, remaining_conditions) where:
            - subject_values: List of IRI strings from subject equality conditions
            - remaining_conditions: List of conditions that should still become FILTER
        """
        subject_values: list[str] = []
        remaining_conditions: list["FilterLike"] = []

        for cond in self._conditions:
            # Check if this is a Condition on the subject variable with equality
            if (
                isinstance(cond, Condition)
                and cond.field_name == SUBJECT_VARIABLE_NAME
                and cond.op == "="
            ):
                subject_values.append(str(cond.value))
            else:
                remaining_conditions.append(cond)

        return subject_values, remaining_conditions

    def _get_pattern_object(self, field_name: str, iri_binding: str | None) -> str:
        """Get the object part of a triple pattern (IRI or variable).

        This helper reduces duplication in pattern building by centralizing
        the logic for generating the object clause of a triple pattern.

        Args:
            field_name: Name of the field (used for variable binding)
            iri_binding: IRI value for direct binding, or None for variable binding

        Returns:
            Either a wrapped IRI (<http://...>) or a SPARQL variable (?field_name)
        """
        if iri_binding:
            return f"<{escape_sparql_iri(iri_binding)}>"
        return f"?{field_name}"

    def _add_field_patterns(self, where_lines: list[str], s_var: str = "?s") -> None:
        """Add patterns for all regular (non-subject) fields.

        Generates required (non-OPTIONAL) patterns when:
        - Field has VALUES binding (constrains the subject variable)
        - Field has required=True
        - Field has IRI filter_by binding (direct triple pattern binding)

        For IRIField with filter_by binding, generates direct pattern:
            ?s <predicate> <IRI> .
        instead of:
            OPTIONAL { ?s <predicate> ?field . }

        For collection field queries, collection fields are handled by
        OPTIONAL subquery patterns (added separately) rather than simple
        OPTIONAL patterns here.
        """
        # Determine which fields to add patterns for
        # Skip collection fields if query has collection fields (handled by subqueries)
        if self._has_collection_fields():
            fields_to_add: dict[str, Any] = self._get_non_collection_regular_fields()
        else:
            fields_to_add = self._get_regular_fields()

        for name, field in fields_to_add.items():
            # Check for IRI filter_by binding (direct pattern instead of variable)
            iri_binding: str | None = self._filter_by_iri_bindings.get(name)

            # Check for IRIField with default value (like rdf_type)
            # These should use the default value in a required pattern to
            # constrain query results
            field_default: Any = getattr(field, "default", None)
            is_iri_field_with_default: bool = (
                isinstance(field, IRIField) and field_default is not None
            )

            # Check if pattern should be required (non-OPTIONAL)
            # A field is required when:
            # 1. It has VALUES binding (so the pattern constrains the subject), OR
            # 2. It has required=True, OR
            # 3. It has IRI filter_by binding (must match for results), OR
            # 4. It's an IRIField with a default value (constrains type/category)
            field_has_values: bool = self._values is not None and name in self._values
            field_is_required: bool = getattr(field, "required", False)
            field_has_iri_binding: bool = iri_binding is not None
            use_required_pattern: bool = (
                field_has_values
                or field_is_required
                or field_has_iri_binding
                or is_iri_field_with_default
            )

            # Get predicate (expand short-form IRI if session available)
            predicate: str = field.predicate
            if self._session:
                predicate = self._session.expand_iri(predicate)
            predicate_wrapped: str = f"<{escape_sparql_iri(predicate)}>"

            # Build the triple pattern
            # Get the object part: IRI binding, default IRI, or variable
            if iri_binding is not None:
                pattern_object: str = self._get_pattern_object(name, iri_binding)
            elif is_iri_field_with_default:
                # Use the default IRI value for required pattern
                default_iri: str = field_default
                if self._session:
                    default_iri = self._session.expand_iri(default_iri)
                pattern_object = f"<{escape_sparql_iri(default_iri)}>"
            else:
                pattern_object = self._get_pattern_object(name, None)

            if name in self._property_paths:
                # Use query-level property path (from .path() method)
                property_path: PropertyPath = self._property_paths[name]
                pattern: str = f"{s_var} {property_path.to_sparql()} {pattern_object} ."
            elif field.is_property_path():
                # Use field-level property path (defined in model field)
                # The predicate already contains the SPARQL property path expression
                pattern = f"{s_var} {field.predicate} {pattern_object} ."
            else:
                # Use simple predicate
                pattern = f"{s_var} {predicate_wrapped} {pattern_object} ."

            # Add pattern with or without OPTIONAL wrapper
            if use_required_pattern:
                where_lines.append(pattern)
                # For IRIField with default, add BIND to make variable available
                if is_iri_field_with_default and not iri_binding:
                    where_lines.append(f"BIND({pattern_object} AS ?{name}) .")
            else:
                where_lines.append(f"OPTIONAL {{ {pattern} }}")

    def _add_iri_filter_by_binds(self, where_lines: list[str]) -> None:
        """Add BIND statements for IRI fields used in filter_by().

        When filter_by() binds an IRI value directly in the triple pattern
        (e.g., ?s <predicate> <IRI> .), we need BIND statements to make the
        variable available in SELECT clause.

        This generates patterns like:
            BIND(<http://example.org/work> AS ?work_iri) .

        The BIND ensures the field variable is available for hydration while
        the direct triple pattern ensures correct filtering semantics.
        """
        for field_name, iri_value in self._filter_by_iri_bindings.items():
            iri_wrapped: str = f"<{escape_sparql_iri(iri_value)}>"
            where_lines.append(f"BIND({iri_wrapped} AS ?{field_name}) .")

    def _add_collection_subquery_patterns(
        self,
        where_lines: list[str],
        s_var: str = "?s",
        subject_values_clause: str = "",
    ) -> None:
        """Add OPTIONAL subquery patterns for collection fields.

        Each collection field gets its own OPTIONAL subquery with internal
        GROUP BY. This avoids cartesian product because each subquery returns
        at most one row per subject (already aggregated).

        When subject_values_clause is provided, it is pushed into each
        subquery's WHERE block so the endpoint can constrain the scan.
        """
        if not self._has_collection_fields():
            return

        collection_fields: dict[str, Any] = self._get_collection_fields()
        # Collect the named graph URIs to use in GRAPH clauses within each
        # subquery. from_graph is included first (it is also declared as
        # FROM NAMED in the outer query by _build_from_clauses so that GRAPH
        # patterns can address it), followed by any explicit from_named URIs.
        subquery_graph_uris: list[str] = []
        if self._from_graph:
            subquery_graph_uris.append(self._from_graph)
        subquery_graph_uris.extend(self._from_named_graphs)
        for name, field_info in collection_fields.items():
            pattern: str = self._compile_collection_subquery_pattern(
                name,
                field_info,
                s_var,
                subject_values_clause,
                subquery_graph_uris or None,
            )
            where_lines.append(pattern)

    def _build_prefix_declarations(self) -> str:
        """
        Build PREFIX declarations from session's prefix registry.

        Returns:
            PREFIX declarations as a string (with trailing newline if non-empty)
        """
        prefix_declarations: str = ""
        if self._session:
            prefix_decls: str = self._session.get_prefix_declarations()
            if prefix_decls:
                prefix_declarations = prefix_decls + "\n"
        return prefix_declarations

    def _build_from_clauses(self) -> str:
        """
        Build FROM and FROM NAMED clauses for the query.

        For from_graph URIs, both FROM <g> (sets the default graph for outer
        WHERE patterns) and FROM NAMED <g> (makes the graph addressable via
        GRAPH <g> in collection subqueries) are emitted. This dual declaration
        is required because SPARQL 1.1 subqueries (SubSelect) have no
        DatasetClause, so subquery scoping must use GRAPH patterns instead of
        FROM, and GRAPH patterns can only address FROM NAMED graphs.

        Returns:
            FROM clauses as a string (with trailing newline if non-empty)
        """
        from_clauses: list[str] = []
        if self._from_graph:
            escaped: str = escape_sparql_iri(self._from_graph)
            from_clauses.append(f"FROM <{escaped}>")
            from_clauses.append(f"FROM NAMED <{escaped}>")
        for named_graph in self._from_named_graphs:
            from_clauses.append(f"FROM NAMED <{escape_sparql_iri(named_graph)}>")

        return "\n".join(from_clauses) + "\n" if from_clauses else ""

    @staticmethod
    def _deduplicate_preserving_order(values: list[str]) -> list[str]:
        """Deduplicate a list while preserving order (first occurrence wins).

        Args:
            values: List of strings to deduplicate.

        Returns:
            Deduplicated list maintaining original order.
        """
        seen: set[str] = set()
        deduplicated: list[str] = []
        for val in values:
            if val not in seen:
                seen.add(val)
                deduplicated.append(val)
        return deduplicated

    def _build_values_clause(
        self, subject_filter_values: list[str] | None = None
    ) -> str:
        """
        Build VALUES clause if values are specified.

        Args:
            subject_filter_values: Optional list of IRI values from subject equality
                conditions (from filter_by on SubjectField). These are merged with
                any existing VALUES on ?s.

        Returns:
            VALUES clause as a string, or empty string if no values
        """
        # Merge subject filter values with existing _values if both exist
        effective_values: dict[str, list[Any]] = {}
        if self._values:
            effective_values = dict(self._values)

        # Handle subject filter values from filter_by() on SubjectField
        if subject_filter_values:
            if SUBJECT_VARIABLE_NAME in effective_values:
                # Merge: combine existing ?s values with filter_by values
                existing_s_values: list[Any] = effective_values[SUBJECT_VARIABLE_NAME]
                merged_s_values: list[str] = (
                    list(existing_s_values) + subject_filter_values
                )
                effective_values[SUBJECT_VARIABLE_NAME] = (
                    self._deduplicate_preserving_order(merged_s_values)
                )
            else:
                # Create new ?s binding from filter_by values
                effective_values[SUBJECT_VARIABLE_NAME] = (
                    self._deduplicate_preserving_order(subject_filter_values)
                )

        if not effective_values:
            return ""

        # Generate variable list
        vars_list: list[str] = [f"?{var}" for var in effective_values.keys()]
        vars_clause: str = " ".join(vars_list)

        # Generate value rows
        rows: list[str] = []
        num_rows: int = len(next(iter(effective_values.values())))
        for i in range(num_rows):
            row_values: list[str] = []
            for var in effective_values.keys():
                value: Any = effective_values[var][i]
                # Format value based on field type (IRI vs literal)
                formatted_value: str = self._format_values_clause_value(var, value)
                row_values.append(formatted_value)
            rows.append(f"({' '.join(row_values)})")

        return f"VALUES ({vars_clause}) {{ {' '.join(rows)} }}"

    def _build_subject_values_clause(
        self, subject_filter_values: list[str] | None = None
    ) -> str:
        """
        Build a VALUES clause containing only ?s bindings for pushing into
        collection subqueries.

        Sources subject IRIs from two places:
        - self._values['s'] (from .values() calls)
        - subject_filter_values (from .filter_by() on SubjectField)

        Deduplicates while preserving order. Returns empty string if no
        subject bindings exist (unconstrained queries are unchanged).

        Args:
            subject_filter_values: Optional list of IRI values from subject
                equality conditions (from filter_by on SubjectField).

        Returns:
            VALUES clause string like ``VALUES (?s) { (<iri1>) (<iri2>) }``
            or empty string if no subject bindings.
        """
        # Collect all subject IRI values
        all_subject_iris: list[str] = []

        # From .values() on ?s
        if self._values and SUBJECT_VARIABLE_NAME in self._values:
            all_subject_iris.extend(self._values[SUBJECT_VARIABLE_NAME])

        # From filter_by() on SubjectField
        if subject_filter_values:
            all_subject_iris.extend(subject_filter_values)

        if not all_subject_iris:
            return ""

        deduplicated: list[str] = self._deduplicate_preserving_order(all_subject_iris)

        if not deduplicated:
            return ""

        # Format as VALUES clause with only ?s
        rows: list[str] = [f"(<{escape_sparql_iri(iri)}>)" for iri in deduplicated]
        return f"VALUES (?s) {{ {' '.join(rows)} }}"

    def _build_where_patterns(self, s_var: str = "?s") -> list[str]:
        """
        Build WHERE clause patterns including VALUES, type, BIND, fields, and filters.

        Subject equality conditions (from filter_by on SubjectField) are converted to
        VALUES clauses for efficient query execution, rather than FILTER clauses which
        cause full table scans on large endpoints.

        Args:
            s_var: Subject variable to use (default: "?s")

        Returns:
            List of WHERE clause pattern strings
        """
        where_lines: list[str] = []

        # Extract subject equality conditions for VALUES clause generation
        # These should NOT become FILTER clauses (causes timeouts on large endpoints)
        subject_filter_values: list[str]
        remaining_conditions: list["FilterLike"]
        subject_filter_values, remaining_conditions = self._extract_subject_conditions()

        # VALUES clause first - provides initial bindings before they're referenced
        # This must come before BIND and field patterns that reference these variables
        # Include subject filter values from filter_by() on SubjectField
        values_clause: str = self._build_values_clause(subject_filter_values or None)
        if values_clause:
            where_lines.append(values_clause)

        # Add BIND clause for SubjectField (if present)
        self._add_subject_field_bind(where_lines, s_var)

        # Add OPTIONAL patterns for non-collection fields
        self._add_field_patterns(where_lines, s_var)

        # Add BIND statements for IRI filter_by fields
        # These make the field variable available in SELECT after direct binding
        self._add_iri_filter_by_binds(where_lines)

        # Compute subject VALUES clause for pushing into collection subqueries.
        # This constrains each subquery to only the specified subjects, preventing
        # full dataset scans on large endpoints (Issue #139).
        subject_values_clause: str = self._build_subject_values_clause(
            subject_filter_values or None
        )

        # Add OPTIONAL subquery patterns for collection fields
        # (each subquery has internal GROUP BY, avoiding cartesian products)
        self._add_collection_subquery_patterns(
            where_lines, s_var, subject_values_clause
        )

        # Language filters for LangString fields
        for field_name, lang_codes in self._lang_filters.items():
            if len(lang_codes) == 1:
                # Single language: FILTER(LANG(?field) = "en")
                safe_lang: str = escape_sparql_literal(lang_codes[0])
                where_lines.append(f'FILTER(LANG(?{field_name}) = "{safe_lang}") .')
            else:
                # Multiple languages: FILTER(LANG(?field) IN ("en", "fr"))
                quoted_langs: str = ", ".join(
                    f'"{escape_sparql_literal(lang)}"' for lang in lang_codes
                )
                where_lines.append(f"FILTER(LANG(?{field_name}) IN ({quoted_langs})) .")

        # Filter conditions (excluding subject equality conditions already in VALUES)
        # All condition types (FilterExpression, LogicalExpression,
        # CollectionContainsExpression, Condition) implement to_sparql(model_class)
        for cond in remaining_conditions:
            where_lines.append(cond.to_sparql(self.model))

        return where_lines

    def _process_aggregate_results(
        self, results: dict[str, Any]
    ) -> list[dict[str, Any]]:
        """
        Process aggregate query results into a list of dictionaries.

        Args:
            results: SPARQL JSON results from query execution

        Returns:
            List of dictionaries with aggregate results
        """
        aggregate_results: list[dict[str, Any]] = []

        for binding in results["results"]["bindings"]:
            result_dict: dict[str, Any] = {}

            # Process aggregate values
            for alias in self._aggregates.keys():
                if alias in binding:
                    value_data: dict[str, Any] = binding[alias]
                    if value_data["type"] == "literal":
                        # Convert datatype if present
                        if "datatype" in value_data:
                            result_dict[alias] = self._hydrator._convert_datatype(
                                value_data["value"], value_data["datatype"]
                            )
                        else:
                            result_dict[alias] = value_data["value"]

            # Process GROUP BY fields if present
            for group_field in self._group_by_fields:
                if group_field in binding:
                    group_value_data: dict[str, Any] = binding[group_field]
                    if group_value_data["type"] == "uri":
                        result_dict[group_field] = group_value_data["value"]
                    elif group_value_data["type"] == "literal":
                        if "datatype" in group_value_data:
                            result_dict[group_field] = self._hydrator._convert_datatype(
                                group_value_data["value"], group_value_data["datatype"]
                            )
                        else:
                            result_dict[group_field] = group_value_data["value"]
                else:
                    # GROUP BY field is unbound (no data) - include as None
                    result_dict[group_field] = None

            aggregate_results.append(result_dict)

        return aggregate_results

    def _get_lang_var_expression(self, field_name: str) -> str | None:
        """
        Generate LANG() extraction expression for a field if it supports language tags.

        Uses the polymorphic get_lang_select_expression() method on the field
        to determine if and how to generate the LANG() expression.

        Args:
            field_name: Name of the field to check

        Returns:
            LANG() expression string like "(LANG(?name) AS ?name_lang)" if field
            supports language tags (e.g., LangString), None otherwise
        """
        field_info = self.model._rdf_fields.get(field_name)
        if field_info is None:
            return None
        # Use polymorphic method - LangString returns the expression, others return None
        return field_info.get_lang_select_expression(field_name)

    def _build_select_clause_aggregate(self, distinct_modifier: str) -> str:
        """
        Build SELECT clause for aggregate queries.

        Args:
            distinct_modifier: "DISTINCT " or "" depending on query settings

        Returns:
            Complete SELECT clause string for aggregate query
        """
        agg_selects: list[str] = []

        for alias, (func, field_name) in self._aggregates.items():
            if field_name == "*":
                # Special case for COUNT(*)
                agg_selects.append(f"({func}(*) as ?{alias})")
            else:
                # Regular field aggregate
                agg_selects.append(f"({func}(?{field_name}) as ?{alias})")

        # Add GROUP BY fields to SELECT if present
        for group_field in self._group_by_fields:
            agg_selects.append(f"?{group_field}")

        return f"SELECT {distinct_modifier}" + " ".join(agg_selects)

    def _build_select_clause_collection(
        self, s_var: str, distinct_modifier: str
    ) -> str:
        """
        Build SELECT clause for collection field queries.

        Collection fields use OPTIONAL subqueries with internal GROUP BY.
        The SELECT references the concatenated result variables from subqueries.

        Args:
            s_var: Subject variable (e.g., "?s")
            distinct_modifier: "DISTINCT " or "" depending on query settings

        Returns:
            Complete SELECT clause string for collection query
        """
        select_vars: list[str] = [s_var]

        # Add SubjectField if present
        if self.model._subject_field_name:
            select_vars.append(f"?{self.model._subject_field_name}")

        # Non-collection fields: use direct variables with LANG() for LangString
        for name, field_info in self._get_non_collection_regular_fields().items():
            select_vars.append(f"?{name}")
            lang_expr: str | None = self._get_lang_var_expression(name)
            if lang_expr:
                select_vars.append(lang_expr)

        # Collection fields: reference the result variables from subqueries
        # For LangStringList, value and language are combined in single variable
        collection_fields: dict[str, Any] = self._get_collection_fields()
        for name in collection_fields:
            concat_var: str = f"{name}{COLLECTION_CONCAT_SUFFIX}"
            select_vars.append(f"?{concat_var}")

        return f"SELECT {distinct_modifier}" + " ".join(select_vars)

    def _build_select_clause_regular(self, s_var: str, distinct_modifier: str) -> str:
        """
        Build SELECT clause for regular (non-aggregate, non-collection) queries.

        Args:
            s_var: Subject variable (e.g., "?s")
            distinct_modifier: "DISTINCT " or "" depending on query settings

        Returns:
            Complete SELECT clause string for regular query
        """
        select_vars: list[str] = [s_var]

        for name, field_info in self.model._rdf_fields.items():
            select_vars.append(f"?{name}")
            lang_expr: str | None = self._get_lang_var_expression(name)
            if lang_expr:
                select_vars.append(lang_expr)

        return f"SELECT {distinct_modifier}" + " ".join(select_vars)

    def compile(self) -> str:
        s_var: str = self.model.subject_var()

        # Add DISTINCT modifier if requested
        distinct_modifier: str = "DISTINCT " if self._distinct else ""

        # Build SELECT clause using appropriate helper based on query type
        if self._aggregates:
            select_clause: str = self._build_select_clause_aggregate(distinct_modifier)
        elif self._has_collection_fields():
            select_clause = self._build_select_clause_collection(
                s_var, distinct_modifier
            )
        else:
            select_clause = self._build_select_clause_regular(s_var, distinct_modifier)

        # Build query components using helper methods
        prefix_declarations: str = self._build_prefix_declarations()
        from_clause: str = self._build_from_clauses()
        where_lines: list[str] = self._build_where_patterns(s_var)
        where: str = "\n  ".join(where_lines)
        q: str = (
            f"{prefix_declarations}{select_clause}\n"
            f"{from_clause}WHERE {{\n  {where}\n}}\n"
        )

        # Add GROUP BY clause (only for explicit group_by(), not for collection queries)
        # Collection queries with scalar subqueries do not need GROUP BY - the
        # aggregation happens inside each subquery independently
        if self._group_by_fields:
            # Explicit GROUP BY (must come before ORDER BY/LIMIT/OFFSET)
            group_by_fields_str: str = " ".join(
                [f"?{field}" for field in self._group_by_fields]
            )
            q += f"GROUP BY {group_by_fields_str}\n"

        # Add ORDER BY clause if specified
        if self._order_by_field:
            order_direction: str = "DESC" if self._order_descending else "ASC"
            q += f"ORDER BY {order_direction}(?{self._order_by_field})\n"
        if self._limit is not None:
            q += f"LIMIT {self._limit}\n"
        if self._offset is not None:
            q += f"OFFSET {self._offset}\n"

        return q

    def all(self) -> list[T]:
        """
        Execute query and return all matching instances.

        Returns:
            List of model instances (may be empty)

        Raises:
            ConfigurationError: If query not bound to session
        """
        if not self._session:
            raise ConfigurationError("Query not bound to session")

        sparql: str = self.compile()
        results: dict[str, Any] | None = self._session.execute_sparql(
            sparql, construct=False
        )

        if not results or "results" not in results:
            return []

        # Handle aggregate queries differently
        if self._aggregates:
            return self._process_aggregate_results(results)  # type: ignore[return-value]

        instances: list[T] = []
        for binding in results["results"]["bindings"]:
            instance: T = self._hydrator.hydrate_instance(binding)
            instances.append(instance)

        return instances

    def first(self) -> T | None:
        """
        Execute query and return first result or None.

        Returns:
            First model instance or None if no results

        Raises:
            ConfigurationError: If query not bound to session
        """
        if not self._session:
            raise ConfigurationError("Query not bound to session")

        # Compile with LIMIT 1 for efficiency
        original_limit: int | None = self._limit
        self._limit = 1
        sparql: str = self.compile()
        self._limit = original_limit  # Restore immediately

        # Execute and get first binding
        results: dict[str, Any] | None = self._session.execute_sparql(
            sparql, construct=False
        )

        if not results or "results" not in results:
            return None

        bindings: list[dict[str, Any]] = results["results"]["bindings"]
        if not bindings:
            return None

        # Handle aggregate queries differently
        if self._aggregates:
            aggregate_results = self._process_aggregate_results(results)
            return aggregate_results[0] if aggregate_results else None  # type: ignore[return-value]

        # Hydrate only the first result
        return self._hydrator.hydrate_instance(bindings[0])

    def one(self) -> T:
        """
        Execute query and return exactly one result.

        Returns:
            Single model instance

        Raises:
            ConfigurationError: If query not bound to session
            QueryError: If zero or multiple results found
        """
        if not self._session:
            raise ConfigurationError("Query not bound to session")

        # Use LIMIT 2 to efficiently detect 0, 1, or multiple results
        original_limit: int | None = self._limit
        self._limit = 2
        sparql: str = self.compile()
        self._limit = original_limit  # Restore immediately

        results: dict[str, Any] | None = self._session.execute_sparql(
            sparql, construct=False
        )

        if not results or "results" not in results:
            raise QueryError(f"No instances of {self.model.__name__} found")

        bindings: list[dict[str, Any]] = results["results"]["bindings"]

        if len(bindings) == 0:
            raise QueryError(f"No instances of {self.model.__name__} found")

        if len(bindings) > 1:
            raise QueryError(
                f"Multiple instances found (at least {len(bindings)}), "
                f"expected exactly one"
            )

        # Handle aggregate queries differently
        if self._aggregates:
            aggregate_results = self._process_aggregate_results(results)
            return aggregate_results[0] if aggregate_results else None  # type: ignore[return-value]

        return self._hydrator.hydrate_instance(bindings[0])

    def one_or_none(self) -> T | None:
        """
        Execute query and return one result or None.

        Returns:
            Single model instance or None

        Raises:
            ConfigurationError: If query not bound to session
            QueryError: If multiple results found
        """
        if not self._session:
            raise ConfigurationError("Query not bound to session")

        # Use LIMIT 2 to efficiently detect 0, 1, or multiple results
        original_limit: int | None = self._limit
        self._limit = 2
        sparql: str = self.compile()
        self._limit = original_limit  # Restore immediately

        results: dict[str, Any] | None = self._session.execute_sparql(
            sparql, construct=False
        )

        if not results or "results" not in results:
            return None

        bindings: list[dict[str, Any]] = results["results"]["bindings"]

        if len(bindings) == 0:
            return None

        if len(bindings) > 1:
            raise QueryError(
                f"Multiple instances found (at least {len(bindings)}), "
                f"expected at most one"
            )

        # Handle aggregate queries differently
        if self._aggregates:
            aggregate_results = self._process_aggregate_results(results)
            return aggregate_results[0] if aggregate_results else None  # type: ignore[return-value]

        return self._hydrator.hydrate_instance(bindings[0])

    def __iter__(self) -> Iterator[T]:
        """
        Allow iteration over query results with lazy evaluation.

        This method provides true lazy iteration by executing the query once
        and yielding results one at a time, rather than loading all results
        into memory at once.

        Usage: for person in session.query(Person):

        Returns:
            Iterator that yields model instances one at a time

        Raises:
            ConfigurationError: If query not bound to session
        """
        if not self._session:
            raise ConfigurationError("Query not bound to session")

        # Compile the query for execution
        sparql: str = self.compile()

        # Execute the query
        results: dict[str, Any] | None = self._session.execute_sparql(
            sparql, construct=False
        )

        if not results or "results" not in results:
            return iter([])  # Empty iterator

        # Handle aggregate queries differently - they can't be iterated
        if self._aggregates:
            # For aggregate queries, we need to process all results at once
            # to properly handle GROUP BY and aggregate functions
            aggregate_results = self._process_aggregate_results(results)
            return iter(aggregate_results)  # type: ignore[arg-type]

        # Create a generator that yields instances one by one
        def result_generator() -> Iterator[T]:
            for binding in results["results"]["bindings"]:
                instance: T = self._hydrator.hydrate_instance(binding)
                yield instance

        return result_generator()

    def count(self) -> int:
        """
        Return count of matching entities using SPARQL COUNT aggregate.

        This method is more efficient than len(query.all()) as it uses
        a COUNT(*) aggregate query that doesn't need to retrieve all
        the actual data.

        Returns:
            Count of matching entities

        Raises:
            ConfigurationError: If query not bound to session
        """
        if not self._session:
            raise ConfigurationError("Query not bound to session")

        # Build a COUNT query instead of fetching all data
        # We need to build a modified version of the query that uses COUNT(*)

        # Build query components using helper methods
        s_var: str = self.model.subject_var()
        distinct_modifier: str = "DISTINCT " if self._distinct else ""

        prefix_declarations: str = self._build_prefix_declarations()
        from_clause: str = self._build_from_clauses()
        where_lines: list[str] = self._build_where_patterns(s_var)
        where: str = "\n  ".join(where_lines)

        # Build the COUNT query
        count_query: str = (
            f"{prefix_declarations}SELECT (COUNT({distinct_modifier}*) as ?count)\n"
            f"{from_clause}WHERE {{\n  {where}\n}}"
        )

        # Execute the COUNT query
        count_results: dict[str, Any] | None = self._session.execute_sparql(
            count_query, construct=False
        )

        if not count_results or "results" not in count_results:
            return 0

        # Extract the count value
        bindings: list[dict[str, Any]] = count_results["results"]["bindings"]
        if not bindings:
            return 0

        count_binding: dict[str, Any] = bindings[0]
        if "count" in count_binding:
            count_data: dict[str, Any] = count_binding["count"]
            if count_data["type"] == "literal":
                return int(count_data["value"])

        return 0


class SPARQLCompiler:
    @staticmethod
    def compile_query(query: Query[Any]) -> str:
        return query.compile()
