Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions src/boring_semantic_layer/calc_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,7 @@ def apply_calc_measures(
real_totals_tbl=None,
agg_specs: dict[str, Any] | None = None,
totals_prefix: str = TOTALS_PREFIX,
totals_base_builder: Any | None = None,
):
"""Re-run each calc-measure lambda against the real aggregated table.

Expand Down Expand Up @@ -553,7 +554,11 @@ def apply_calc_measures(
if real_with_totals is None:
if real_totals_tbl is None:
real_totals_tbl = _build_totals_from_agg_specs(
base_tbl, agg_specs, calc_lambdas, known_measures
base_tbl,
agg_specs,
calc_lambdas,
known_measures,
base_totals_builder=totals_base_builder,
)
if real_totals_tbl is not None:
real_with_totals = _join_totals(
Expand Down Expand Up @@ -847,6 +852,7 @@ def _build_totals_from_agg_specs(
calc_lambdas: dict[str, Any],
known_measures: frozenset[str],
classifications: dict[str, CalcExprAnalysis] | None = None,
base_totals_builder: Any | None = None,
):
"""Build a no-group-by totals table when callers passed ``agg_specs``.

Expand All @@ -855,15 +861,25 @@ def _build_totals_from_agg_specs(
see correctly-recomputed dependencies. Returns ``None`` when there
is no way to construct totals (no ``agg_specs`` supplied or the
specs fail to evaluate against the base).

``base_totals_builder``, when given, replaces the agg-spec re-run as
the source of base totals. The pre-agg join path uses it because
re-running agg specs on a fanned-out join inflates the totals; the
builder aggregates each source table at zero grain instead.
"""
if not agg_specs:
return None
try:
totals_aggs = {n: f(base_tbl) for n, f in agg_specs.items()}
except Exception as exc:
logger.debug("totals aggregation failed to evaluate: %s", exc)
return None
real_totals = base_tbl.aggregate(**totals_aggs)
if base_totals_builder is not None:
real_totals = base_totals_builder()
if real_totals is None:
return None
else:
if not agg_specs:
return None
try:
totals_aggs = {n: f(base_tbl) for n, f in agg_specs.items()}
except Exception as exc:
logger.debug("totals aggregation failed to evaluate: %s", exc)
return None
real_totals = base_tbl.aggregate(**totals_aggs)

if classifications is None:
classifications = classify_calc_lambdas(calc_lambdas, base_tbl, known_measures)
Expand Down
25 changes: 23 additions & 2 deletions src/boring_semantic_layer/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,17 @@ def __getattr__(self, name: str):
return getattr(self._t, name)

def __getitem__(self, name: str):
return getattr(self._t, name)
# Materialized columns win (e.g. prefixed columns already present
# on a post-aggregation result, where the dimension lambda would
# be stale). Fall back to declared dimensions so bracket access
# also resolves prefixed names like t["orders.status"] on tables
# that don't carry them as literal columns yet.
try:
return getattr(self._t, name)
except AttributeError:
if name in self._dims:
return self._dims[name](self._t).name(name)
raise


@frozen
Expand All @@ -128,7 +138,18 @@ def __getattr__(self, key: str):
return getattr(self._t, key)

def __getitem__(self, key: str):
return getattr(self._t, key)
# Materialized columns win (a stale dim/measure lambda must not
# shadow a column that already exists on the result); fall back
# to declared dims/measures for prefixed names like
# t["orders.total"] that aren't literal columns yet.
try:
return getattr(self._t, key)
except AttributeError:
if key in self._dims:
return self._dims[key](self._t)
if key in self._meas:
return self._meas[key](self._t)
raise


# ============================================================================
Expand Down
Loading
Loading