Skip to content

fix: soundness round4#288

Draft
hussainsultan wants to merge 12 commits into
mainfrom
fix/soundness-round4
Draft

fix: soundness round4#288
hussainsultan wants to merge 12 commits into
mainfrom
fix/soundness-round4

Conversation

@hussainsultan

Copy link
Copy Markdown
Collaborator

No description provided.

hussainsultan and others added 11 commits July 18, 2026 09:55
Filter strings, dict predicates, and agent query strings previously built
literals with whichever ibis was importable (xorq-vendored when installed),
mis-composing against tables of the other flavor: equality silently yielded
a constant predicate, ordering raised. Pick the flavor from the actual
table at resolve time, memoize per module, and reject expressions that
resolve to a Python bool (the cross-flavor identity-comparison footgun).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Apply pre-agg filters before grain transforms: range and lambda filters
  compared grain-truncated buckets instead of raw values, silently
  dropping in-range rows (week grain corrupted both directions).
- Date-only time_range ends now cover the whole end day (exclusive bound
  at next midnight); explicit time endpoints keep <= semantics.
- smallest_time_grain long form ("TIME_GRAIN_DAY") was double-prefixed
  in _validate_time_grain, silently disabling grain validation.
- in/not-in dict filters reject bare-string values instead of matching
  individual characters ('not in "gold"' returned the gold rows).
- Only complete ISO date/datetime strings are coerced to typed literals;
  partial strings like "2024" no longer inherit today's month/day.
- limit=0 means LIMIT 0, not "no limit"; bool limits rejected.
- order_by directions validated; 'descending'/'ascending' accepted,
  anything else raises instead of silently sorting ascending.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- with_measures now evicts a same-named measure of the other kind:
  base-first lookup silently served the old definition when a
  redefinition was classified as calc. Self-referential redefinitions
  raise instead of resolving to the old measure.
- get_ibis_module unwraps the resolver proxies passed to filter and
  dimension callables, so the bool-guard's remediation (build literals
  with the table's flavor) actually works inside a filter lambda; the
  error message now shows the constant-predicate escape hatch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ures

A dimension sharing its name with a raw column (amount=lambda t: t.amount*2)
was materialized over the column before filter resolution, so lambda
filters applied the expression twice while dict/string filters applied it
once, and any filter's presence made measures silently aggregate the
mutated column.

- Filter enrichment no longer materializes dimensions over existing
  columns (overwrite_existing=False): the resolver applies the dimension
  expression exactly once against raw values, and measures keep reading
  the raw column. All three filter spellings now agree.
- Dict/string filter callables resolve against the dimension-aware
  resolver instead of unwrapping it through _ensure_xorq_table.
- group_by on a shadowing dimension combined with a measure that reads
  the shadowed column is inherently ambiguous and now raises with a
  rename suggestion; identity dimensions and measures over other columns
  are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The final column selection silently filtered requested group keys and
measures to whatever the pre-agg result happened to contain. On
cross-joined models (no dimension bridge) this returned a result that
ignored the requested grouping entirely — one unlabeled grand-total row
in place of per-group rows. Missing requested columns now raise with an
explanation and restructuring guidance; de-fanned cross-join grand
totals are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pre-aggregation built many-side measures on the RAW unjoined table
unless cross-table filter routing happened to force a join-key bridge.
Orphan rows (NULL FK, or a key matching no left-side row) were silently
counted in grand totals and many-side-only group-bys while mixed
group-bys excluded them, so sum-over-groups stopped matching the
ungrouped grand total on the same model.

The key-bridge inner join against the joined table's distinct join keys
now applies unconditionally to every join_many table with requested
measures, not only when needs_bridge is set. When no shared join-key
column is available (or in the chasm fallback with no root-side key to
bridge through) the query raises instead of silently counting rows the
LEFT JOIN can never produce.

Flights star-schema grand-total expectations updated to LEFT JOIN
ground truth (14 SCE-origin flights match no airport row); verified
against raw SQL. Regression suite: test_soundness_round4_join_many.py,
including a NULL-dimension-value case guarding the round-2 C1 fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tions

lift_inline_reductions rewrote EVERY WindowFunction whose func was a
lifted base-column reduction to Field(totals_vt, anon) — the t.all()
grand-totals shape — without checking whether the window was empty.
A measure like

    t.amount.sum() / t.amount.sum().over(window(group_by=t.region))

silently returned shares of the GRAND total instead of per-region
shares (the emitted SQL had no PARTITION BY).

Route each windowed base reduction by shape instead:

- empty window: grand totals, unchanged (ADR 0001 t.all semantics) —
  except when the window is the whole calc expression and the reduction
  reads only group-key columns, where base-row totals and output-row
  windows are both plausible readings; that ambiguous shape now raises.
- non-empty window, decomposable reduction (sum/count/min/max/any/all)
  with every window key a group key of the aggregation: re-aggregate
  the lifted per-group value over the output rows inside the same
  window, partition/order keys remapped to output columns (count
  re-aggregates with sum). Mirrors the already-correct measure-
  reference window path.
- anything else (non-decomposable reduction, non-group-key window key):
  raise WindowedBaseReductionError pointing at the measure-reference
  form. _compile_aggregation re-raises it past the apply-time fallback
  so it cannot be silently re-evaluated at different grain.

No silent wrong answers remain for this family: every non-empty-window
case either computes the ground-truth value or raises loudly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
nest= lambdas received a scope over raw columns only: the canonical
measure-name form (t.group_by("sku").aggregate("total_qty")) raised
column-not-found, and inner aggregations whose result names collided
with raw columns executed but were silently replaced by one raw struct
per source row.

Nest lambdas now receive the aggregation's semantic source table, so
measure names, dimension names, and inline lambdas/Deferreds resolve
exactly like a top-level aggregate:

- t.group_by(...).aggregate(...) becomes a NestAggSpec: the inner query
  is re-grouped at (outer keys + inner keys) grain over its own source
  chain (inner filters included) and compiled as its own semantic
  aggregation; SemanticAggregateOp._to_untagged_with_nest collects its
  rows into one array-of-structs per outer group and attaches them with
  a null-safe left join on the outer keys (NULL dimension groups keep
  their rows; groups the inner filter emptied keep a NULL array).
- Bare t.group_by(...) keeps the pinned per-row struct collection
  (duplicates included); group_by now also flattens list arguments so
  the documented list form works on semantic tables.
- Unsupported nested shapes (order_by/limit, transformed bare group_by,
  raw ibis returns) raise NotImplementedError naming the shape — silent
  raw-row structs are gone.

Adds test_soundness_round4_nest.py pinning canonical, aliased-inline,
raw-name-collision, multi-key, filtered-outer, inner-filter, no-outer-
key, and bare forms against pandas ground truth, plus the loud errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rebase reconciled our NestAggSpec pipeline with main's parallel nest
fix (1e7fa98), adopting its filter/order_by/limit support after the
inner aggregate. The regression test that pinned order_by-raises is now
a positive ordered-array assertion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hussainsultan
hussainsultan force-pushed the fix/soundness-round4 branch from fa2fe3a to e277287 Compare July 18, 2026 14:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant