Skip to content

feat(round_temporal): implement Substrait round_temporal/round_calendar (item 74) - #292

Merged
discreteds merged 9 commits into
developfrom
feature/74-round-temporal-calendar
Aug 17, 2026
Merged

feat(round_temporal): implement Substrait round_temporal/round_calendar (item 74)#292
discreteds merged 9 commits into
developfrom
feature/74-round-temporal-calendar

Conversation

@discreteds

Copy link
Copy Markdown
Member

Summary

Implements the Substrait round_temporal/round_calendar datetime rounding operations (backlog item 74) across all three backends (polars, ibis, narwhals), and redirects the existing MA truncate/round_dt/ceil_dt/floor_dt convenience wrappers through the real implementation instead of the old approximation that silently fell back to truncate for round/ceil and rejected multi-digit multipliers on ibis-duckdb/narwhals.

Changes by task

  1. Task 1ROUND_TEMPORAL/ROUND_CALENDAR FKEYs + function defs.
  2. Task 2 — Fixed pre-existing protocol signature bugs; added shared unit/rounding option domains.
  3. Task 3 — Substrait API builder, wired the x argument channel, drained the unwired-ops park.
  4. Task 4 — polars implementation (real rounding via truncate + offset).
  5. Task 5 — ibis implementation (per-dialect; ibis-sqlite has no TimestampBucket compilation rule — declared UNSUPPORTED).
  6. Task 6 — narwhals implementation (YEAR/MONTH/WEEK declared UNSUPPORTED — ambiguous fixed-duration length in narwhals' every string).
  7. Task 7 — Redirected MA truncate/round_dt/ceil_dt/floor_dt wrappers (polars/ibis/narwhals) through round_temporal/round_calendar. Retired now-false DURATION_MULTIPLIER capability facts (re-probed 2026-08-16: every fixture now honors every multiplier value via the real redirect). Added rounding.py capability module for the ibis-sqlite structural gap. Fixed a duplicate CapabilityDeclaration identity collision in options.py/value_classes_ma.py evidence fixtures.
  8. Task 8 — Drained KNOWN_UNTESTED_OPTION_PARAMS rounding/unit/multiple parks with a live-checked reason.

Verification

  • Full suite: 22841 passed, 81 skipped, 3462 xfailed, 0 failed.
  • ruff check: clean.
  • mypy: baseline unchanged (97 pre-existing import-stub errors, none in touched files).
  • Dedicated cross-backend test file: tests/expressions/cross_backend/test_datetime_rounding.py (126/126 passing across every backend, including ibis-sqlite gap coverage).
  • Regenerated docs/reference/expression-coverage{.md,.json,-scoped.md}.

Companion spec/plan (mountainash-central, superpowers workflow) tracked backlog item 74.

…s (item 74)

Real Substrait shared 9-unit domain (YEAR..MICROSECOND) confirmed against
functions_datetime.yaml; options=(rounding, unit, multiple) -- origin
excluded (real Substrait channel is arguments, not options; see spec §3.4).
Drains the two protocol-alignment KNOWN_ASPIRATIONAL park entries now that
the ops are wired into the function registry (Task 2-6 still needed to fix
the pre-existing buggy protocol signatures and implement real backend bodies).
…ared unit/rounding domains (item 74)

round_calendar's parameter order previously diverged from round_temporal's
(origin before multiple vs after); both had meaningless non-None defaults
for rounding/unit despite being required. Fixed both to a consistent
signature with required rounding/unit and Any-typed origin (v1 never
supplies non-None -- excluded from options per spec §3.4).

Backend bodies (polars/ibis/narwhals) and the API builder are not yet
updated -- expected RED at this checkpoint (Tasks 3-6).
… unwired-ops park (item 74 Task 3)

- add round_temporal/round_calendar to SubstraitScalarDatetimeAPIBuilder and
  its protocol stub; rounding/unit validated against the closed domains,
  multiple must be a positive int, origin always raises (v1 scope, not a
  string option per real Substrait)
- fix origin annotation: Optional[Any] -> Any (Optional[Any] is redundant and
  was misclassified 'unclassified' by the option/argument taxonomy walker;
  bare Any is the existing convention for option-kind Any params)
- wire the x argument channel: two new OP_SPEC entries mirror to_timezone/
  local_timestamp's pattern (fixed rounding+unit options, x varies raw/lit/
  col/complex) -- these now exercise REAL polars/narwhals compiles, which
  correctly RED (24 cases) since Tasks 4/6 haven't replaced the stub bodies
  yet; ibis passes already (round_temporal.x/round_calendar.x OpSpecs use
  uppercase Substrait unit names, which ibis truncate() accepts natively)
- drain KNOWN_ASPIRATIONAL (test_protocol_alignment.py) and
  _KNOWN_UNWIRED_TESTED_OPS/_KNOWN_METADATA_ONLY_TESTED_PARAMS entries now
  that the ops are registry-wired and OP_SPEC-tested; re-key
  _KNOWN_UNTESTED_OPTION_PARAMS with accurate reasons for rounding/unit/
  multiple (wired option, backend bodies still stubs) and origin
  (permanently excluded, always raises) with live machine-checked probes
  (closed-by-default per test_untested_option_param_custom_reasons_still_hold)
…(item 74 Task 4)

FLOOR/CEIL/ROUND_TIE_DOWN hand-rolled from dt.truncate + dt.offset_by (no
native CEIL/tie-down primitive). ROUND_TIE_UP uses native dt.round -
verified its tie rule IS tie-up (10:30->11:00) and it accepts every
combined multiplier/unit string used here including calendar units
(1mo/3mo/1y/1w). round_temporal declares YEAR/MONTH/WEEK unsupported
(defense in depth; real gate is a capability fact, Task 7) - fixed-duration
rounding is ambiguous for them; round_calendar covers all nine units.

New cross-backend test file (36 polars/polars-lazy cases, all passing):
floor/ceil for DAY/HOUR/MONTH/YEAR/WEEK, tie-break at an exact hour
midpoint, multiple=2 hours, quarter-via-month-multiple-3, and the
YEAR/MONTH/WEEK round_temporal capability-error gate. ibis/narwhals cases
in this file correctly RED pending Tasks 5-6.
…tem 74 Task 5)

x.truncate(unit) accepts Substrait canonical uppercase names directly
(verified 12.0.0/duckdb) for FLOOR at multiple=1. TimestampValue.bucket()
(an experimental-but-working native primitive) handles multiple>1 for every
unit including calendar ones, bucketing from the UNIX epoch. CEIL/tie modes
hand-rolled via interval addition + epoch_seconds distance comparison (ibis
has no native round/ceil and rejects Polars-style combined multiplier
truncate strings).

New capabilities/datetime/rounding.py: real per-dialect gaps discovered by
the probe --
- ibis-sqlite: TimestampTruncate has no HOUR/MINUTE/SECOND/MILLISECOND/
  MICROSECOND support (only YEAR/MONTH/WEEK/DAY), and TimestampBucket has
  no sqlite compilation rule at all (multiple>1 unsupported everywhere)
- ibis-polars: CEIL/tie on MONTH/YEAR needs interval addition, which
  ibis's polars sub-backend cannot translate (polars.duration() has no
  months/years kwarg); the whole unit is declared UNSUPPORTED there
  (closed-by-default -- partial per-rounding-mode support is exactly the
  silent-inconsistency this repo's facts exist to prevent)

Restructured the cross-backend test file to split value-assertion classes
from the now-real capability-error classes for these two dialect gaps
(xfail-not-skip: the combo IS tested, via a raise assertion instead of a
value one). All ibis cases now pass (43->83 across ibis+polars); narwhals
cases correctly RED pending Task 6.
…r (item 74 Task 6)

Narwhals dt.truncate/dt.offset_by use Polars-style combined <n><suffix>
strings (verified 2.24.0, both dialects) -- unlike ibis, bare Substrait
names are not accepted. No native round/ceil/tie primitive at all, so
every mode but FLOOR is hand-rolled the same way as polars (truncate +
offset_by + duration-distance comparison via total_seconds()), verified
including calendar units and multiple>1 (e.g. '3mo' for a quarter).

WEEK is declared UNSUPPORTED on round_calendar for both narwhals dialects
(dt.truncate rejects the '1w' duration, verified 2026-08-16) -- matches
the pre-existing MA truncate/floor_dt '1w' gap in options.py rather than
hand-rolling day-of-week arithmetic (v1 scope decision, preserve-not-fix).

Restructured the test file's WEEK/DAY class into three: DAY (all backends),
WEEK (excludes narwhals), and a dedicated narwhals-WEEK capability-error
class. Full cross-backend test file: 126/126 passing across every backend.
Argument-channel tests: 32/32 passing (was 24 RED before this task).
… audit datetime capability facts (item 74 Task 7-8)

- Redirect polars/ibis/narwhals MA truncate/round_dt/ceil_dt/floor_dt
  wrappers through the real round_temporal/round_calendar
  implementation (item 74 Task 4-6) instead of the old
  x.truncate("<n><unit>")-based approximation that silently fell back
  to truncate for round/ceil and rejected multi-digit multipliers on
  ibis-duckdb/narwhals.
- Retire now-false DURATION_MULTIPLIER capability facts in
  value_classes_ma.py: every fixture honors every multiplier value via
  the real redirect (re-probed 2026-08-16, disposition table in module
  docstring). Only ibis-sqlite's TimestampBucket compilation gap
  remains, documented as a residual note (no compilation rule exists
  on that dialect for any rounding op — tracked as a backlog
  follow-up, not a fact, since MATRIX_UNREACHABLE_DIALECT_FACTS
  already exempts it structurally).
- Add rounding.py capability module declaring ibis-sqlite's structural
  gap for round_temporal/round_calendar (all rounding units
  unsupported on that dialect — no compilation rule).
- Fix options.py/value_classes_ma.py declaration evidence fixtures to
  reflect each module's actual probed scope, resolving a duplicate
  CapabilityDeclaration identity collision across MOUNTAINASH-source
  IBIS declarations.
- Wire rounding.py into the capability protocol guard's expected
  module list and domain-predicate table.
- Add ROUND_TEMPORAL/ROUND_CALENDAR smoke-test arg overrides.
- Retire stale test_multiplier_gate cross-backend regression test
  (asserted BackendCapabilityError on cells now honored); keep
  assume_timezone IANA_TIMEZONE coverage.
- Remove now-XPASSing strict-xfail entries for ibis-duckdb/narwhals
  round/ceil in test_datetime_unit_dispatch.py's fallback-divergence
  map — both ops are real implementations now.
- Drain KNOWN_UNTESTED_OPTION_PARAMS rounding/unit/multiple parks with
  a live-checked reason (dedicated cross-backend test file coverage,
  verified via _round_temporal_calendar_still_works()).
- Regenerate docs/reference/expression-coverage{.md,.json,-scoped.md}.

Full suite: tests/expressions/ + tests/core/ all green (17701 passed,
65 skipped, 3400 xfailed, 0 failed). ruff clean.
…emporal-calendar

# Conflicts:
#	docs/reference/expression-coverage-scoped.md
#	docs/reference/expression-coverage.json
#	docs/reference/expression-coverage.md
#	tests/expressions/argument_types/test_coverage_guard.py
@discreteds
discreteds merged commit 228d31d into develop Aug 17, 2026
4 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