Skip to content

refactor phase 5: API surface — one query(), eager validation, YAML schema, no how= - #302

Merged
hussainsultan merged 6 commits into
mainfrom
refactor/phase5-api-surface
Aug 19, 2026
Merged

refactor phase 5: API surface — one query(), eager validation, YAML schema, no how=#302
hussainsultan merged 6 commits into
mainfrom
refactor/phase5-api-surface

Conversation

@hussainsultan

Copy link
Copy Markdown
Collaborator

Phase 5 of the refactor, stacked on #301. Per maintainer direction this PR makes clean breaking changes (no deprecation shims) with tests, examples, and docs updated in-step. Full suite: 1743 passed, exit 0; make examples-core green; SQL snapshots byte-identical.

One query(), one compare_periods()

The four drifted copies (SemanticModel / SemanticJoin / SemanticFilter / op-level SemanticJoinOp — the last silently missing time_grains) are replaced by one documented implementation on the SemanticTable base. Every semantic expression now accepts the identical parameter set, and grouped/aggregated results gain .query()/.compare_periods() where they previously raised AttributeError.

Eager validation with suggestions

query() now fails at construction on unknown dimension/measure names:

  • before: XorqTypeError: Column 'revenu' is not found in table. Existing columns: 'region', 'amount' — at execute, leaking physical columns
  • after: UnknownFieldError: Unknown measure 'revenu'. Declared measures: ['revenue']. Did you mean: measure 'revenue'? — at construction

Raw table columns and unique suffix matches on joined models remain valid dimension spellings, so nothing legitimate got stricter.

YAML configs are schema-validated at load

The audit's silent-typo class is closed: dimension: / measurez: used to load as models with zero dimensions; they now raise DefinitionError naming the model, the key, and accepted spellings with did-you-mean hints. Field sections must be mappings, smallest_time_grain values are validated at load (not first query), and a non-left how: in a YAML join fails at load with the left-join+filter guidance.

how= is gone

join_one/join_many accepted how= only to raise for anything but "left" — dead surface. The parameter is removed from every wrapper, the op-level methods, YAML, and serialization reconstruction (stored how values are informational; semantic joins are always LEFT, join_cross carries how="cross" internally).

Worth reading in the diff: the removal sweep initially stripped how="left" from two pandas merge / raw-ibis join fixtures, which default to INNER — the soundness suites flagged the changed row counts immediately and those fixtures were restored untouched. That's the invariant suites doing exactly their job.

Namespace + protocol

  • to_tagged/from_tagged move to boring_semantic_layer.serialization; the five graph_* helpers demote to boring_semantic_layer.graph_utils. to_untagged stays top-level (the documented plain-ibis escape hatch, used by examples).
  • .name/.description are defined once on the SemanticTable base: aggregates/limits answer None instead of raising AttributeError: 'Table' object has no attribute 'name'.

Deferred (named, not forgotten)

Fluent time_grain/time_range/having methods (the JSON path — and now every expression's .query() — already provides them), index() returning an expression wrapper, StrEnums for grains/directions, and the full docstring pass over expr.py's public surface.

🤖 Generated with Claude Code

@hussainsultan
hussainsultan force-pushed the refactor/phase4-serialization branch from 495bbba to 8eeb595 Compare August 18, 2026 23:15
@hussainsultan
hussainsultan force-pushed the refactor/phase5-api-surface branch from 6d6792e to e352f20 Compare August 18, 2026 23:15
@hussainsultan
hussainsultan force-pushed the refactor/phase4-serialization branch from 8eeb595 to 9be1b17 Compare August 18, 2026 23:27
@hussainsultan
hussainsultan force-pushed the refactor/phase5-api-surface branch from e352f20 to 0ebde8b Compare August 18, 2026 23:27
@hussainsultan
hussainsultan force-pushed the refactor/phase4-serialization branch from 9be1b17 to 048161e Compare August 18, 2026 23:37
@hussainsultan
hussainsultan force-pushed the refactor/phase5-api-surface branch from 0ebde8b to 893448d Compare August 18, 2026 23:37
@hussainsultan
hussainsultan force-pushed the refactor/phase4-serialization branch from 048161e to 29bcdc4 Compare August 18, 2026 23:44
@hussainsultan
hussainsultan force-pushed the refactor/phase5-api-surface branch from 893448d to 1e14d07 Compare August 18, 2026 23:44
@hussainsultan
hussainsultan force-pushed the refactor/phase4-serialization branch from 29bcdc4 to e790de3 Compare August 18, 2026 23:50
@hussainsultan
hussainsultan force-pushed the refactor/phase5-api-surface branch from 1e14d07 to 1fb09b6 Compare August 18, 2026 23:50
@hussainsultan
hussainsultan force-pushed the refactor/phase4-serialization branch from e790de3 to a0865e5 Compare August 19, 2026 00:05
@hussainsultan
hussainsultan force-pushed the refactor/phase5-api-surface branch from 1fb09b6 to 4e40e80 Compare August 19, 2026 00:05
hussainsultan and others added 5 commits August 18, 2026 20:34
…base

The four drifted copies (SemanticModel / SemanticJoin / SemanticFilter /
op-level SemanticJoinOp — the last missing time_grains entirely) are
replaced by a single documented implementation on the base class, so
every semantic expression accepts the same parameters. Grouped and
aggregated results gain .query()/.compare_periods() for free.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unknown names now raise UnknownFieldError at query construction with
did-you-mean suggestions, instead of surfacing at execute() as a raw
backend error listing physical columns. Raw table columns and unique
suffix matches on joined models remain valid dimension spellings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unknown top-level model keys (the classic 'dimension:' / 'measurez:'
typos that used to load silently as empty sections) now raise
DefinitionError naming the model, the key, and accepted spellings with
did-you-mean hints; field sections must be mappings; and
smallest_time_grain values are validated at load instead of at query
time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
join_one/join_many accepted how= but raised for anything except
'left' — dead surface that only added a way to get an error. The
parameter is gone from every wrapper, the op-level methods, YAML
(where a non-left how now fails at load with the left-join+filter
guidance), and serialization reconstruction (stored 'how' values are
informational; semantic joins are always LEFT, join_cross carries
how='cross' internally). Tests, examples, and docs updated.

Two ground-truth near-misses caught by the suites and reverted: the
sweep initially stripped how='left' from pandas merge / raw ibis join
fixtures, which default to INNER — the soundness suite flagged the
changed row counts immediately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- to_tagged/from_tagged move to boring_semantic_layer.serialization
  (xorq-interop jargon out of the front door); to_untagged stays — it's
  the documented plain-ibis escape hatch and examples use it
- the five graph_* helpers demote to boring_semantic_layer.graph_utils
- .name/.description defined once on the SemanticTable base: aggregates
  and limits now answer None instead of raising AttributeError blaming
  ibis's Table

Examples and docs updated to the new import paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hussainsultan
hussainsultan force-pushed the refactor/phase5-api-surface branch from 4e40e80 to f5cdede Compare August 19, 2026 00:34
@hussainsultan
hussainsultan changed the base branch from refactor/phase4-serialization to main August 19, 2026 00:34
@hussainsultan hussainsultan reopened this Aug 19, 2026
malloy_interop.py imports boring_semantic_layer.malloy (exists only on
feat/malloy-interop) and breaks make examples + lint on this branch
standalone; all three return to untracked local files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hussainsultan
hussainsultan marked this pull request as ready for review August 19, 2026 00:43
@hussainsultan
hussainsultan merged commit 8255925 into main Aug 19, 2026
9 checks passed
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