diff --git a/src/boring_semantic_layer/expr.py b/src/boring_semantic_layer/expr.py index db75766..d11fd2c 100644 --- a/src/boring_semantic_layer/expr.py +++ b/src/boring_semantic_layer/expr.py @@ -109,12 +109,14 @@ def to_untagged(expr): + from .ops import _rebind_to_canonical_backend + if isinstance(expr, SemanticTable): - return expr.op().to_untagged() + return _rebind_to_canonical_backend(expr.op().to_untagged()) result = safe(lambda: expr.to_untagged())() if isinstance(result, Success): - return result.unwrap() + return _rebind_to_canonical_backend(result.unwrap()) raise TypeError(f"Cannot convert {type(expr)} to Ibis expression") diff --git a/src/boring_semantic_layer/ops.py b/src/boring_semantic_layer/ops.py index 038bd0c..c163639 100644 --- a/src/boring_semantic_layer/ops.py +++ b/src/boring_semantic_layer/ops.py @@ -2510,7 +2510,13 @@ def _to_untagged_with_preagg( ) # --- 3. Partition agg_specs by source table --- - partitioned = _partition_agg_specs_by_source(dict(plan.agg_specs), all_roots) + # Partition by the join's per-table roots, not ``all_roots``. When a + # wrapper SemanticTableOp from ``SemanticJoin.with_measures()`` / + # ``with_dimensions()`` is in the tree, ``all_roots`` collapses to a + # single name=None root, so prefixed aggregates would all fall into the + # unprefixed bucket and bypass per-grain pre-aggregation. + partition_roots = _find_all_root_models(join_op) + partitioned = _partition_agg_specs_by_source(dict(plan.agg_specs), partition_roots) # --- 4. Pre-aggregate each source table on its raw table --- _preagg_results: list = []