This page explains the query path of the analytics module. src/cluster_erp/modules/analytics/registry.py holds the validated catalogue, src/cluster_erp/modules/analytics/query_builder.py builds safe aggregate queries with correctness gates, and src/cluster_erp/modules/analytics/recipe_engine.py with recipe_schemas.py runs declarative preparation recipes. How the catalogue is filled by other modules is on analytics facts.
In short¶
- A user asks with semantic keys only: a fact, measures, dimensions, filters, order, and a page (
src/cluster_erp/modules/analytics/schemas.py:1-11,src/cluster_erp/modules/analytics/schemas.py:47-61). - Table and column names come only from module declarations; values are bound parameters. There is no string concatenation into SQL (
src/cluster_erp/modules/analytics/query_builder.py:5-10). - Four gates refuse a number that would be wrong: non-additive sums, semi-additive sums over an unpinned axis, mixed currencies, and dates without a grain (
src/cluster_erp/modules/analytics/query_builder.py:11-23). - A recipe is a stored list of steps from a closed vocabulary; the engine records rows in and rows out for each step (
src/cluster_erp/modules/analytics/recipe_engine.py:1-21,src/cluster_erp/modules/analytics/recipe_schemas.py:1-33).
How it works¶
The recipe and query pipeline¶
flowchart TD
A["POST /analysis/run with AnalysisSpec"] --> C["AnalyticsService.catalogue: collect and visible_to"]
P["POST /analysis/prepare with RecipeDefinition"] --> C
C --> F["FactCatalogue.get fact"]
F --> S{"summaries.plan fits"}
S -->|yes| QS["query_builder._build_from_summary"]
S -->|no| QB["query_builder.build"]
QB --> G["gates: additivity, currency, grain"]
QS --> G
G --> X["execute, cap at MAX_ROWS"]
X --> R["AnalysisResult with source summary or live"]
F --> RE["RecipeEngine.run"]
RE --> ST["apply steps, count rows in and out"]
ST --> EX["check expectations"]
EX --> RR["RecipeRunResult with observations"]In prose:
- Both entry points first build the caller's catalogue:
registry.collectasks every module, andvisible_tokeeps only fact sets whoseread_permissionthe caller holds (src/cluster_erp/modules/analytics/service.py:55-62,src/cluster_erp/modules/analytics/registry.py:100-109). - For a run,
summaries.plandecides whether the pre-aggregated cells can answer; otherwise the query runs on the fact table (src/cluster_erp/modules/analytics/service.py:139-148). See reports, summaries, and cross. query_builder.buildbuilds one relation with all declared columns, adds grouped dimensions, measures, filters, the live filter, order, and a page of at mostMAX_ROWS10000 plus one row to detect truncation (src/cluster_erp/modules/analytics/query_builder.py:71-96,src/cluster_erp/modules/analytics/query_builder.py:375-470).- The result states whether it came from
summaryorlive, and why the live path was taken (src/cluster_erp/modules/analytics/service.py:157-168). - For a recipe, the engine starts from every declared column keyed by its semantic name, applies the live filter, runs each enabled step, and measures the row count before and after (
src/cluster_erp/modules/analytics/recipe_engine.py:195-262).
Correctness gates¶
| Gate | Refuses | Error code | Source |
|---|---|---|---|
| Additivity | sum or avg of a non_additive measure |
ANALYTICS_NON_ADDITIVE |
src/cluster_erp/modules/analytics/query_builder.py:242-272 |
| Semi-additive axis | sum of a semi_additive measure unless its non_additive_on dimension is pinned with eq |
ANALYTICS_SEMI_ADDITIVE_AXIS |
src/cluster_erp/modules/analytics/query_builder.py:274-288 |
| Currency | an aggregate of a monetary measure without a currency breakdown or a single-currency eq filter; count and count_distinct are exempt |
ANALYTICS_MIXED_CURRENCY |
src/cluster_erp/modules/analytics/query_builder.py:290-335 |
| Grain | a date dimension without an explicit, declared grain | ANALYTICS_UNKNOWN_GRAIN |
src/cluster_erp/modules/analytics/query_builder.py:337-373 |
Only eq pins a value; in does not, because two selected currencies summed are still a mixed total (src/cluster_erp/modules/analytics/query_builder.py:62-68). Two further rules hold: no round() in SQL, and monetary medians use percentile_disc so that every figure is an amount that occurred (src/cluster_erp/modules/analytics/query_builder.py:24-31).
Recipes¶
A recipe (RecipeDefinition) has one input, steps, expectations, and a limit of up to 100000 rows (src/cluster_erp/modules/analytics/recipe_schemas.py:255-283).
| Element | Values | Source |
|---|---|---|
| Implemented step operations | filter, rename, drop, sort, dedupe, derive, cast, fill, group |
src/cluster_erp/modules/analytics/recipe_schemas.py:182-184 |
| Deferred operations, refused | join, union, pivot, unpivot, window, split, merge, lookup, flag_outliers |
src/cluster_erp/modules/analytics/recipe_schemas.py:191-192 |
| Expectations | not_null, unique, accepted_values, row_count_between, no_negative, each with severity error or warning |
src/cluster_erp/modules/analytics/recipe_schemas.py:200-219 |
| Error policy per step | fail, null, keep_original, quarantine |
src/cluster_erp/modules/analytics/recipe_schemas.py:44 |
Each step has a stable id, a presentation seq, a note, and a disabled flag; a disabled step is skipped instead of being removed (src/cluster_erp/modules/analytics/recipe_schemas.py:47-60, src/cluster_erp/modules/analytics/recipe_engine.py:221). Step ids must be unique (src/cluster_erp/modules/analytics/recipe_schemas.py:274-281). A step names semantic keys; a key that the fact set never declared is rejected (src/cluster_erp/modules/analytics/recipe_engine.py:69-93).
Why it is built this way¶
- The query builder docstring names one promise: never produce a wrong number (
src/cluster_erp/modules/analytics/query_builder.py:1-4). - The recipe schema keeps
opa closed vocabulary because it leaves no injection surface and keeps the space of next-step suggestions small (src/cluster_erp/modules/analytics/recipe_schemas.py:12-17). - Joins are not implemented because a join without a row-multiplication gate would multiply every measure (
src/cluster_erp/modules/analytics/recipe_engine.py:17-21,src/cluster_erp/modules/analytics/recipe_schemas.py:186-190).
Rules and boundaries¶
- A recipe accepts exactly one input; more than one is refused until the join gate exists (
src/cluster_erp/modules/analytics/recipe_schemas.py:262-272). - An order key must be one of the selected dimensions or measures (
src/cluster_erp/modules/analytics/query_builder.py:454-463). - The registry refuses physical names that do not match a strict identifier pattern, as defence in depth (
src/cluster_erp/modules/analytics/registry.py:41-48).
Interaction with other parts¶
| Contract | Direction | Purpose | Source |
|---|---|---|---|
DescribeAnalyticsFacts |
analytics emits | collect the fact sets | src/cluster_erp/modules/analytics/registry.py:253 |
AnalyticsFactSet |
modules answer | declare facts, dimensions, measures, permission, live filter | src/cluster_erp/core/contracts.py:2126-2154 |
Limits and open points¶
RecipeInput.as_ofis stored with the recipe as the snapshot moment; a read ofas_ofinrecipe_engine.pywas not found (src/cluster_erp/modules/analytics/recipe_schemas.py:244-253).- A result carries the generated SQL text in
sql(src/cluster_erp/modules/analytics/service.py:164,src/cluster_erp/modules/analytics/query_builder.py:543-552).
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| analysis spec | AnalysisSpec |
A request in semantic keys: fact, measures, dimensions, filters, order, page. | Analiz tanımı | src/cluster_erp/modules/analytics/schemas.py:47 |
| recipe | RecipeDefinition |
A stored, re-runnable list of preparation steps over one fact set. | Tarif | src/cluster_erp/modules/analytics/recipe_schemas.py:255 |
| step observation | StepObservation |
The rows in, rows out, and delta recorded for one recipe step. | Adım gözlemi | src/cluster_erp/modules/analytics/recipe_schemas.py:284 |
Related pages¶
Verified against commit 92f8051 on 2026-11-13.