Developer Internals Guide¶
This guide is intended for engineers who want to contribute to the fc-selector library core or understand how it works "under the hood".
1. Query Lifecycle¶
When a user makes a request like GET /api/posts?$filter=status eq 'published', the following happens:
-
View/Selector (
odata_selector.py):- Receives the query string.
- Calls
parse_odata_query(protocol layer) to get an intermediateODataQuerystructure. - Converts
ODataQuery->QueryIntent(Core). This step normalizes names and structure. - If there are string filters, they are parsed to AST (
parse_filter) and injected intoQueryIntent.
-
Executor (
django/executor.py):- Receives the
QueryIntent. - Filters: Passes the AST to
AstToDjangoQVisitor. This traverses the tree and builds a complexQ()object. - Optimization (Expand/Select): Analyzes relationships.
- If it's a direct relation (FK), adds
select_related. - If it's a reverse or M2M relation, creates a
Prefetchobject. - Recursion: For
Prefetch, the Executor calls itself (creates a newDjangoExecutor) to apply filters/ordering to the subquery.
- If it's a direct relation (FK), adds
- Receives the
2. Working with the AST¶
The AST (fc_selector/core/ast/nodes.py) is the abstract representation of filters.
Adding a new OData function¶
If you want to support a new function, for example years_between(date1, date2):
- Define the AST node: Make sure
Callcan represent it (usually yes). - Update the Parser (
protocols/odata/parsers/grammar.py): If special syntax is needed. If it's a standard function, the generic parser will already detect it asCall. - Implement in the Visitor (
django/visitors/filter_visitor.py):- Add a method
djangofunc_years_between(self, arg1, arg2). - This method must return a Django
Expression(e.g.,FuncorExpressionWrapper).
- Add a method
def djangofunc_years_between(self, start, end):
# Simplified example
return ExtractYear(self.visit(end)) - ExtractYear(self.visit(start))
3. Debugging¶
If a query fails or returns incorrect data:
- Verify the
QueryIntent: Set a breakpoint atDjangoExecutor.execute. Inspect theintentobject. Is it well-formed? Does it have the correct AST? - Verify the Visitor: If the filter fails, look at
AstToDjangoQVisitor.visit. How is the node being translated? - Exceptions:
- If you get a generic 500, the Visitor likely failed with an uncontrolled exception.
- Make sure the Visitor throws
core.exceptions.SelectorError(or subclasses) so the adapter can convert it to a 400 Bad Request.
4. Hybrid Values Mode¶
When values_mode = True on a selector (the default), the system uses an optimized execution path for queries with $expand on forward relations.
How it works¶
ODataSelectorcallsDjangoExecutor.try_hybrid()before the standard path.try_hybrid()usesHybridValuesBuilder.classify_relations()to split expand into forward vs reverse.- If all relations are forward (FK/OneToOne), the builder:
- Collects flattened field names:
['id', 'title', 'author__id', 'author__name'] - Runs
select_related('author').values(...)— a single SQL query - Reconstructs nested DTOs from the flat dict
- If any reverse relations exist,
try_hybrid()returnsNoneand the selector falls back to standard mode.
Key files¶
fc_selector/django/hybrid_values_builder.py: All hybrid logic (field collection, unflatten, DTO construction)fc_selector/django/executor.py:try_hybrid()entry pointfc_selector/django/selector/odata_selector.py:values_modegating
Property fields¶
@property fields are silently skipped by the builder (get_field_safe() returns None for non-DB fields). They remain UNSET in the resulting DTO. Set values_mode = False to use standard mode with full model instantiation.
5. Key Directory Structure¶
fc_selector/core: DO NOT TOUCH (unless you're changing the global architecture). This is where the contracts live.fc_selector/protocols: This is where string parsing logic lives.fc_selector/django: This is where the ORM magic lives.executor.py: The unified entry point (execute()for standard,try_hybrid()for hybrid).hybrid_values_builder.py: Hybrid values mode implementation.visitors/: AST to Django Q translation.
6. Tests¶
Whenever you touch Core or Executor, run the full integration suite:
These tests verify that the complete chain (Parser -> Intent -> Executor -> DB) works correctly.