Project Architecture¶
FC Selector follows the principles of Hexagonal Architecture (Ports & Adapters) to decouple business logic (data selection) from external protocols (OData) and persistence infrastructure (Django ORM).
Overview¶
The system is divided into three main layers:
- Protocol Layer (Input): Handles parsing of external requests (e.g., OData String).
- Core Layer (Domain): Defines the query intent (
QueryIntent) and data structures (DTOs,AST). It's pure Python with no framework dependencies. - Infrastructure Layer (Output): Executes the query against the database (
DjangoExecutor).
Data Flow¶
graph TD
A[Client Request] -->|OData String| B(OData Parser)
B -->|Converts| C{QueryIntent}
C -->|Core DTO| D[Django Executor]
D -->|Visitor Pattern| E[Django QuerySet]
E -->|SQL| F[(Database)]
Key Components¶
1. Core (fc_selector/core)¶
The system's heart. No external dependencies (neither Django nor DRF).
- QueryIntent (
core.intent): The canonical object representing "what the user wants". Contains filters, field selection, ordering, and pagination in a protocol-agnostic way. - AST (
core.ast): Abstract Syntax Tree to represent complex filter expressions (AND, OR, functions...). - QueryBuilder (
core.query_builder): Fluent API to buildQueryIntentprogrammatically. Supports dependency injection of custom filter parsers. - Exceptions (
core.exceptions): Domain errors (e.g.,FieldNotFoundError,InvalidFieldError,TypeMismatchError).
2. Protocol: OData (fc_selector/protocols/odata)¶
Acts as an input adapter.
- Parsers: Convert OData strings (
$filter=name eq 'X') into Core AST objects.- Uses thread-local caching to reuse lexer/parser instances for better performance.
- Validates maximum filter length (
MAX_FILTER_LENGTH=4000) to prevent DoS attacks.
- Converters: Transform OData-specific structures into a clean
QueryIntent.
3. Infrastructure: Django (fc_selector/django)¶
Acts as an output adapter (backend).
- DjangoExecutor (
django.executor): The execution brain. Receives aQueryIntentand applies necessary transformations to theQuerySetrecursively.- Handles
filter()using Visitors. - Handles
select_relatedandprefetch_relatedautomatically to avoid N+1 problems. - Handles
only()to optimize field loading. try_hybrid()— attempts hybrid values mode for forward-only$expand, returning DTOs directly.execute()— standard path, always returns aQuerySet.
- Handles
- HybridValuesBuilder (
django.hybrid_values_builder): Executes queries using.values()with$expandsupport for all relation types (Forward/Reverse FK, M2M). Collects flattened__fields for forward relations and executes separate queries for reverse/M2M relations, then reconstructs nested DTOs. - AstToDjangoQVisitor (
django.visitors): Translates AST nodes (e.g.,Eq,Contains) to DjangoQobjects. Includes field validation for security. - ODataSelector (
django.selector): The public facade that developers use. Coordinates parsing and execution. Controls execution path viavalues_modeMeta option. - Introspection Utilities (
django.utils.introspection): Safe model field access helpers likeget_field_safe(),is_forward_relation().
Security¶
The system implements several security measures:
Field Validation¶
- Private field blocking: Fields starting with
_are automatically blocked. - Field existence validation: Invalid field names raise
InvalidFieldError. - Allowed fields whitelist: Optional set of allowed fields for fine-grained control.
Input Validation¶
- Filter length limit: Expressions exceeding
MAX_FILTER_LENGTHare rejected. - Type-safe parsing: Strong typing prevents injection attacks.
Performance¶
Parser Caching¶
- Thread-local storage for lexer/parser instances.
- Avoids repeated instantiation overhead on each request.
Query Optimization¶
- Automatic
select_relatedfor forward relations. - Automatic
prefetch_relatedfor reverse/many relations. only()to fetch only requested fields.
Hybrid Values Mode¶
- Uses
.values()with__notation for forward FK/OneToOne expands — 2-5x faster than standard mode. - Uses 1+N query strategy for reverse FK/M2M relations — faster than model instantiation.
- Controlled per-selector via
values_modeMeta option (default:True). - Falls back to standard mode only when
values_mode = False. - See Hybrid Values Mode for details.
Extensibility¶
This architecture allows:
- Protocol Swap: We could implement a GraphQL parser that generates a
QueryIntent, and the entire system would work without backend changes. - Backend Swap: We could create a
SqlAlchemyExecutorthat takes the sameQueryIntentand executes it against another ORM. - Parser Injection: The
QueryBuilderaccepts custom filter parsers via dependency injection.
Error Handling¶
Exceptions are managed from inside out:
1. Executor/Visitor: Throws Core exceptions (core.exceptions).
2. Applier/Selector: Catches Core exceptions and translates them to protocol exceptions (ODataFilterError) with appropriate HTTP error codes.
Exception hierarchy: