Map Search SQL Builder (SQLSpec)¶
This guide documents the SQLSpec-based map search query builder used by the API map search flow. It is designed to be readable, stable, and easy to extend.
Location¶
- Builder:
apps/api/utilities/map_search.py
How the query is built¶
The builder composes a single SELECT statement that can include:
- Filter-driven CTEs
- An
intersection_map_idsCTE (AND semantics across CTE filters) - A main SELECT with core map fields and computed columns
- A
LEFT JOIN LATERALto the latest incomplete playtest meta row - WHERE filters, ordered for stability
- ORDER BY and pagination
CTE model¶
CTEs are built in a fixed order to keep the SQL predictable and ensure filter interactions are consistent:
- mechanics
- restrictions
- creator_ids
- creator_names (one CTE per name, then intersected)
- minimum_quality
- medals
- completions
If any CTEs are active, they are intersected via:
When filters.code is set and filters.force_filters is False, CTEs are
skipped. This allows fast, direct lookups by map code.
WHERE clause ordering¶
The WHERE clause is appended in a consistent order. Add new conditions at the end unless the new filter must appear earlier to preserve parameter ordering or execution characteristics.
Current order:
- code
- playtesting
- playtest_filter (thread_id NULL / NOT NULL)
- difficulty range
- difficulty exact
- archived
- hidden
- official
- map_name
- category
- playtest_thread_id
- finalized_playtests
SELECT columns¶
The SELECT list is intentionally stable. Do not reorder columns unless you have an API-level reason to do so. It includes:
- Core map fields (
m.*subset) pm.thread_idfrom the lateral jointimesubquery for user completion time- Ratings, playtest JSON, creators JSON, guides array, medals JSON
- Mechanics and restrictions arrays (COALESCE with empty arrays)
COUNT(*) OVER() AS total_results
Most computed columns are raw SQL fragments to keep the output stable and avoid sqlglot rewriting.
Pagination¶
Pagination uses SQLSpec's paginate() helper on the compiled SQL statement.
- When
filters.return_allisTrue, pagination is skipped.
Parameter ordering¶
Parameters are registered as the query is assembled. If you introduce new parameters, add them at a point in the build process that keeps the order stable and predictable.
Adding a new filter¶
- Add a field to
MapSearchFilters. - Decide whether it belongs in:
- a CTE (set-based filtering), or
- the WHERE clause (direct filtering).
- Implement the CTE in
_build_ctes()or add the WHERE condition in_apply_where_clauses().
Use a CTE if the filter should participate in the intersection behavior with other set-based filters.
Adding a new column¶
- Add a helper method that returns a SQL fragment or expression.
- Add it to
_build_select_columns()in the correct position. - If the column needs parameters, register them on the query with
query.add_parameter()and use placeholders in the fragment.
Adding a new intersection source¶
If a new filter should participate in the intersection_map_ids CTE:
- Add the CTE builder method.
- Register it in
_build_ctes()in the correct order. - Ensure its output is
SELECT map_id ...so the intersection works without additional transformations.