Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
15c5d8c
fix: unify join_one default to how="left" across all receivers
hussainsultan Jul 9, 2026
f437660
fix: index() selector failures raise instead of indexing every field
hussainsultan Jul 9, 2026
4f3f27c
fix: deferred dimension joins keep requested grain and never drop dims
hussainsultan Jul 9, 2026
e3f98ba
fix: mean(where=...) decomposition honors its filter under pre-aggreg…
hussainsultan Jul 9, 2026
9212a81
fix: null-safe group-key joins in the pre-agg re-join
hussainsultan Jul 9, 2026
032b10d
fix: compute non-decomposable measures at exact target grain
hussainsultan Jul 9, 2026
51cc18d
fix: bare derived-dimension filters resolve on the joined table
hussainsultan Jul 9, 2026
63099ef
fix: cross-table compound filters push row-precise legs; OR raises
hussainsultan Jul 9, 2026
08387f2
fix: group_by().filter().aggregate() keeps the grouping keys
hussainsultan Jul 9, 2026
eb8a4d7
fix: derived dims as group keys survive the pre-agg path
hussainsultan Jul 9, 2026
fc301ea
fix: dimension-only shortcut accepts prefixed and derived-dim filters
hussainsultan Jul 9, 2026
e31eb5b
fix: serialize _source_join so wrapper round-trips keep pre-aggregation
hussainsultan Jul 9, 2026
203b3f4
fix: validate recovered join leaves on round-trip instead of silently…
hussainsultan Jul 9, 2026
ac91995
harden: totals attachment failures raise instead of silently degrading
hussainsultan Jul 9, 2026
9c68b6d
test: regression suite for round-2 soundness findings
hussainsultan Jul 9, 2026
c66027c
fix(examples): use dotted join-prefix in index-across-joins selector
hussainsultan Jul 9, 2026
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
2 changes: 1 addition & 1 deletion examples/dimensional_indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ def get_autocomplete_suggestions(prefix: str, limit: int = 10):
flights_with_origin = flights.join_one(airports, lambda f, a: f.origin == a.code)

joined_index = (
flights_with_origin.index(s.cols("carrier", "airports__state"))
flights_with_origin.index(s.cols("carrier", "airports.state"))
.order_by(lambda t: t.weight.desc())
.limit(15)
.execute()
Expand Down
40 changes: 21 additions & 19 deletions src/boring_semantic_layer/calc_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,9 +414,12 @@ def compile_calc_measure(
)
for col_name in totals_schema:
prefixed = f"{totals_prefix}{col_name}"
target_name = prefixed if prefixed in rwt_columns else col_name
if target_name in rwt_columns:
subs[Field(totals_vt_op, col_name)] = Field(rwt_op, target_name)
# Only substitute the real TOTALS column. Falling back to the
# unprefixed per-group column silently replaced the grand total
# with the group's own value (every share became 1.0) and made
# the unresolved-reference guard below unreachable.
if prefixed in rwt_columns:
subs[Field(totals_vt_op, col_name)] = Field(rwt_op, prefixed)

rewritten = op.replace(subs)

Expand Down Expand Up @@ -635,21 +638,19 @@ def attach_windowed_totals(
try:
agg_expr = agg_specs[name](new_base)
except Exception as exc:
logger.debug(
"could not evaluate agg_spec for %r when attaching windowed totals: %s",
name,
exc,
)
continue
raise TotalsNotAvailableError(
f"Could not evaluate the aggregation for {name!r} while "
"attaching totals; a skipped totals column would silently "
"substitute the per-group value for the grand total."
) from exc
try:
windowed = agg_expr.over(ibis_mod.window())
except Exception as exc:
logger.debug(
"could not wrap %r in window() for windowed totals: %s",
name,
exc,
)
continue
raise TotalsNotAvailableError(
f"Could not window the aggregation for {name!r} while "
"attaching totals; a skipped totals column would silently "
"substitute the per-group value for the grand total."
) from exc
col = f"{totals_prefix}{name}"
new_base = new_base.mutate(**{col: windowed})
arbitrary_specs[col] = (lambda t, _c=col: t[_c].arbitrary())
Expand Down Expand Up @@ -772,10 +773,11 @@ def attach_calc_totals(
else:
totals_expr = fn
except Exception as exc:
logger.debug(
"calc-of-calc totals evaluation failed for %r: %s", calc_name, exc
)
continue
raise TotalsNotAvailableError(
f"Could not evaluate totals for calc measure {calc_name!r}; "
"a skipped totals column would silently substitute the "
"per-group value for the grand total."
) from exc
col = f"{totals_prefix}{calc_name}"
real_agg_tbl = real_agg_tbl.mutate(**{col: totals_expr})

Expand Down
18 changes: 15 additions & 3 deletions src/boring_semantic_layer/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1107,7 +1107,7 @@ def join_one(
self,
other: SemanticModel,
on: Callable[[Any, Any], ir.BooleanValue] | str | Deferred | Sequence[str | Deferred],
how: str = "inner",
how: str = "left",
) -> SemanticJoin:
"""Join with one-to-one relationship semantics."""
return SemanticJoin(
Expand Down Expand Up @@ -1243,7 +1243,7 @@ def join_one(
self,
other: SemanticModel,
on: Callable[[Any, Any], ir.BooleanValue] | str | Deferred | Sequence[str | Deferred],
how: str = "inner",
how: str = "left",
) -> SemanticJoin:
"""Join with one-to-one relationship semantics."""
return SemanticJoin(
Expand Down Expand Up @@ -1289,6 +1289,18 @@ def __init__(self, source: SemanticTableOp, keys: tuple[str, ...]) -> None:
op = SemanticGroupByOp(source=source, keys=keys)
super().__init__(op)

def filter(self, predicate: Callable) -> SemanticGroupBy:
"""Filter rows before aggregation, keeping the grouping keys.

A filter between group_by and aggregate is pre-aggregation row
filtering, so it commutes with the grouping. The inherited filter
wrapped the group-by op itself, and aggregate() — which only
recovers keys from its direct source — silently dropped the
requested grouping.
"""
filtered = SemanticFilter(source=self.op().source, predicate=predicate)
return SemanticGroupBy(source=filtered.op(), keys=self.op().keys)

@property
def source(self):
return self.op().source
Expand Down Expand Up @@ -1469,7 +1481,7 @@ def join_one(
self,
other: SemanticModel,
on: Callable[[Any, Any], ir.BooleanValue] | str | Deferred | Sequence[str | Deferred],
how: str = "inner",
how: str = "left",
) -> SemanticJoin:
"""Join with one-to-one relationship semantics."""
return SemanticJoin(
Expand Down
5 changes: 4 additions & 1 deletion src/boring_semantic_layer/nested_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,10 @@ def join_tables(by_cols: Iterable[str], tables: list) -> Any:
by_cols_set = set(by_cols)

def join_step(left, right):
predicates = [left[c] == right[c] for c in by_cols]
# Null-safe equality: group keys can legitimately be NULL (real NULL
# dim values, or keys minted by an outer join). Plain == drops those
# groups from every table but the first.
predicates = [left[c].identical_to(right[c]) for c in by_cols]
right_cols = [c for c in right.columns if c not in by_cols_set]
right_select = [right[c] for c in right_cols]
return left.left_join(right, predicates).select([left] + right_select)
Expand Down
Loading
Loading