Skip to content

fix(tesseract): compose grain.include with rolling_window - #11639

Open
waralexrom wants to merge 4 commits into
masterfrom
tesseract-rolling-window-grain-include
Open

fix(tesseract): compose grain.include with rolling_window#11639
waralexrom wants to merge 4 commits into
masterfrom
tesseract-rolling-window-grain-include

Conversation

@waralexrom

@waralexrom waralexrom commented Aug 25, 2026

Copy link
Copy Markdown
Member

Summary

A multi-stage measure declaring both grain and rolling_window silently ignored the declared grain: the inner stage was grouped by the query's granularity instead, and with no time dimension in the query the outer aggregation was dropped entirely. Nothing failed — the model compiled, /meta was clean, and the query returned a plausible wrong number. Reported as CORE-789, reproduced standalone against Postgres.

The reported model computes a time-weighted return: a per-day log return summed into a year-to-date window. Only a query asking for the time dimension exactly at the declared grain was correct; granularity: month, a KPI tile with no time dimension, and a group-by on a non-time dimension all returned -0.0667 instead of -0.34 — a single factor over a fully collapsed aggregate, never linked by day.

Changes

  • grain.include now reaches the rolling base. The base member is built by unrolling the measure, which drops its multi-stage properties, so the recursion planning the base CTE never saw the grain. The base rolling state is extended with grain.include after make_rolling_base_state; the frame still keys off the base time dimension, so the extra grain only splits rows the outer aggregation merges back.

  • The no-time-dimension case keeps its aggregation. That branch returns the rolling description directly — no RollingWindow node, hence no RollingMerge render modifier, hence no outer stage to carry the aggregation — while unrolling also collapses the aggregate kind to a calculated one, so type: sum vanished too. New strip_rolling_window transform drops only the window, and the measure is planned as its plain multi-stage self, which is what a window resolving to a single bucket computes.

  • grain.exclude / grain.keep_only are honoured where they can be and rejected where they cannot. They were silently ignored next to a window. With no time dimension there is no frame to build, so the measure goes through the ordinary multi-stage path, which narrows the grain and broadcasts it back — that combination now works. With a time dimension the narrowed value would have to be broadcast onto the query grid and the rolling node has no side enumerating it, so the query is refused.

    The refusal is on the narrowing happening, not on the keys being declared: exclude of a member the grain does not carry subtracts nothing, and keep_only listing everything the query groups by intersects to the same list. Such queries answer exactly as a measure with no keys would, and are left alone. The message names the older reduce_by / group_by spellings too, since those compile into the same lists and a model may contain neither of the grain.* words. An empty keep_only does narrow — it is an intersection, so group_by: [] collapses the grain to a grand total rather than meaning "no key given".

  • QueryProperties::add_dimensions deduplicates against time dimensions. It deduplicated only within dimensions, so a grain.include naming a granularity the query already groups by rendered the same column twice and the query failed as ambiguous. This was broken on the non-rolling path too. A time dimension's full_name pins its granularity, so the check is exact — a bare returns.day and returns.day.day stay distinct grains.

Testing

Both suites were shown failing on the pre-fix code before the fix was written.

Rusttests/integration/multi_stage/rolling_window_grain.rs, 11 tests asserting values against Postgres, not SQL shape: all four query forms from the report (granularity: day, granularity: month, no time dimension, group-by on a non-time dimension), a granularity-qualified grain.include, the ambiguous-column regression, narrowing honoured without a time dimension, two inert narrowing keys answering as a keyless measure, and the three rejection paths (keep_only, reduce_by, empty keep_only). Before the fix, 3 of the 4 value tests failed with exactly the reported -0.0667. Full suite: 1283 passed, 0 failed.

JSpackages/cubejs-schema-compiler/test/integration/postgres/multi-stage-grain-rolling-window.test.ts, the same four forms on the same model. Verified failing by rebuilding the native addon from the reverted Rust sources: 3 of 4 failed with -0.066667. Also ran multi-stage*, postgres-cumulative-measures, rolling-window-offset-no-granularity, bucketing, calc-groups, calendars, custom-granularities, cube-views, member-expression, multiple-join-paths, pre-aggregations*, sql-generation (135 tests), sql-generation-logic and yaml-compiler under CUBEJS_TESSERACT_SQL_PLANNER=true — all green.

On the legacy planner as an oracle: it is not one here. Under CUBEJS_TESSERACT_SQL_PLANNER=false all four forms are wrong including the control measure that declares no window (-0.20 vs -0.34) — the v1 planner does not implement grain: at all, which is why the existing multi-stage-grain.test.ts skips every case on it. The new JS test follows that convention.

Known gaps, deliberately not addressed here

  • Supporting grain.exclude / grain.keep_only alongside a time dimension needs a broadcast side on MultiStageRollingWindow (the node has no keys_input field and its processor never reads one), or a window wrapper mirroring MultiStageCalculationWindowFunction::Window. A prototype got most of the way — narrowing the rolling description's state plus filtering the keys UNION to refs covering every key dim — but a narrowed measure queried alone still has no source for the grid. Left for a follow-up rather than shipped half-working.
  • A to_date window with no time dimension never bounds the date range: replace_date_range_for_rolling_window_without_granularity returns early when both trailing and leading are None. A dateRange spanning two years is reported as year-to-date. Pre-existing and shared with plain non-multi-stage rolling measures — verified identical there — so changing it is its own decision.

waralexrom and others added 3 commits August 24, 2026 17:28
A multi-stage measure declaring both `grain.include` and `rolling_window`
computed its inner stage at the query's granularity instead of the declared
one, and lost its outer aggregation entirely when the query carried no time
dimension. Nothing failed: the model compiled and the query returned a
plausible wrong number.

Two independent losses in the rolling path. The base member is built by
unrolling the measure, which drops the multi-stage properties, so the
recursion that plans the base CTE never saw the grain — extend the base
rolling state with `grain.include` instead. And with no time dimension the
rolling description is returned directly, so no outer stage carries the
aggregation, while unrolling also collapses the aggregate kind to a
calculated one; plan such a measure as its plain multi-stage self, which is
what a window resolving to a single bucket computes.

`grain.exclude` / `grain.keep_only` next to a rolling window were silently
ignored the same way. Narrowing the reported grain needs the narrowed value
broadcast back onto the query grid, which the rolling assembly has no side to
do, so they are now rejected with a message rather than quietly dropped.

`QueryProperties::add_dimensions` deduplicated only within `dimensions`, so a
`grain.include` naming a granularity the query already groups by rendered the
same column twice and the query failed as ambiguous. A time dimension's full
name pins its granularity, so deduplicating added dimensions against the time
dimensions by full name is exact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…error

`reduce_by` and `group_by` compile into the same lists as `grain.exclude` and
`grain.keep_only`, so a model written in the older spelling hits the rejection
while the message names keys that appear nowhere in it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`keep_only` is an intersection, so an empty list narrows the grain to nothing
rather than meaning no key was given — `partition_filter` collapses on it. The
rolling-window guard tested for a non-empty list and let it through, so the
measure was computed at the unnarrowed grain and reported as if the key had
been honoured. `exclude` subtracts and an empty list there really is a no-op,
so only `keep_only` changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@waralexrom
waralexrom requested a review from a team as a code owner August 25, 2026 12:25
@github-actions github-actions Bot added rust Pull requests that update Rust code javascript Pull requests that update Javascript code labels Aug 25, 2026
@claude

claude Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @waralexrom's task in 6m 15s —— View job


Review: no blockers — 0 high, 2 medium, 3 low. 4 inline comments posted.

Full review
  • Gather context (diff, per-commit history, changed files)
  • Review Rust planner changes (try_plan_rolling_window, add_dimensions, strip_rolling_window)
  • Verify the claims the fix rests on (is_cumulative, time-dim full_name, grain key mapping)
  • Check for existing models that the new rejection would break
  • Review test coverage (Rust integration + JS)
  • Post inline comments and summary

What I verified rather than took on trust

The three load-bearing claims in the description all hold:

  • No recursion risk from strip_rolling_window. is_cumulative() is literally is_rolling_window() (measure_symbol.rs:343), so the stripped measure re-entering make_queries_descriptionstry_plan_rolling_window returns None immediately. If is_cumulative ever grows a second disjunct (running totals), this becomes an infinite recursion — worth a comment at the strip_rolling_window call site, since the safety is non-local.
  • "A time dimension's full_name pins its granularity" is exact. time_dimension_symbol.rs:79 builds full_name as {base}_{granularity_or_day}. The _day default for a granularity-less time dimension does not create a false match, because QueryProperties.time_dimensions is filtered to granular entries at construction (query_properties_compiler.rs:66) — I checked this specifically, since a non-granular time dim isn't a grouping key and deduping an include against one would have silently re-introduced the original wrong-number bug.
  • The error message names the right legacy spellings. build_grain_from_legacy (symbols/common/multi_stage.rs:158-163) maps reduce_byexclude, group_bykeep_only, add_group_byinclude. Correct in both directions.

I also swept the repo for models that combine rolling_window with a narrowing grain key: the only ones are the new fixtures. rolling_sum_7d_reduce_time (integration_multi_stage.yaml:693) puts reduce_by on a non-rolling parent of a rolling measure, which the guard doesn't touch — so with_rolling_window.rs::test_rolling_and_reduce_by is unaffected. The rejection breaks no existing test or documented example.

Medium

  1. The guard rejects one case the fix itself makes work. It sits above the time_dimensions.is_empty() early return, but in that branch the measure is planned as strip_rolling_window(&measure) down the ordinary multi-stage path, which does implement exclude/keep_only. So a KPI tile over a rolling_window + reduce_by measure errors even though no RollingWindow node is built. Uniform rejection is defensible; the inline comment argues the opposite. Pick one and say so. (inline)

  2. No docs change for a new hard restriction. docs-mintlify/reference/data-modeling/measures.mdx:639 documents grain and :696 documents the group_by/reduce_by migration, with no mention that two of the three keys are now rejected next to rolling_window. A user migrating reduce_bygrain.exclude on a rolling measure hits a query-time error with nothing in the reference to explain it. One <Warning> in the grain section plus a line in the migration table would cover it. Same for the release note: this turns a wrong answer into a hard failure, which is right, but it is a behavior break for anyone who has the combination today.

Low

  1. add_dimensions is now a silently-dropping setter, and its correctness at the other call site (multi_stage_query_planner.rs:677) depends on partition_filter/set_time_dimensions having run first. That ordering is load-bearing and invisible from the setter — worth pinning in the doc comment. (inline)

  2. The seed's day DATE makes the two grain spellings (returns.day vs returns.day.day) indistinguishable in the data, even though they take different paths through the new dedup. A TIMESTAMP column with two times inside a day keeps the expected -0.34 and actually separates them. (inline)

  3. Third hand-rolled pipe-table parser in this test tree; worth hoisting assert_measures into test_fixtures/test_utils. (inline)

Coverage gaps (minor)

Not requested, but the cheap additions: grain.include naming a non-time dimension next to a window (every fixture case includes a time dimension), and an include at a granularity coarser than the query's (include: returns.day.month with granularity: day), which is the one shape where the "extra grain only splits rows the outer aggregation merges back" argument is least obvious.

Praise

The failure mode here — compiles clean, /meta clean, plausible wrong number — is the worst kind, and the response is right-sized: value assertions against Postgres rather than SQL snapshots (snapshots are exactly what let this through), all four reported query shapes covered, and the three now-rejected shapes asserted on the error text. Calling out in the description that the legacy planner is not an oracle here, with the reason, is the kind of thing that usually gets discovered painfully by the next person. The strip_rolling_window / unroll_rolling split is a clean way to express "the window collapsed" as distinct from "the window was peeled off", and the comments explain why rather than what throughout.

Not verified

I could not run cargo check, the Rust integration suite, or the JS suite in this environment (no build permissions), so I reviewed the changes by reading. Compilation looks sound: MeasureSymbol derives Clone (measure_symbol.rs:156), field access is in-module for transforms, and HashSet is already imported in query_properties.rs:24.

· branch [`tesseract-rolling-window-grain-include`](https://github.com/cube-js/cube/tree/tesseract-rolling-window-grain-include)

Comment on lines 718 to 732
pub fn add_dimensions(&mut self, dimensions: Vec<Rc<MemberSymbol>>) {
let time_dimension_names = self
.time_dimensions
.iter()
.map(|d| d.clone().resolve_reference_chain().full_name())
.collect::<HashSet<_>>();
let added = dimensions.into_iter().filter(|d| {
!time_dimension_names.contains(&d.clone().resolve_reference_chain().full_name())
});
self.dimensions = self
.dimensions
.iter()
.cloned()
.chain(dimensions.into_iter())
.chain(added)
.unique_by(|d| d.clone().resolve_reference_chain().full_name())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dedup itself is sound — TimeDimensionSymbol::full_name() is format!("{}_{}", base.full_name(), granularity_or_day) (time_dimension_symbol.rs:79), and QueryProperties.time_dimensions only ever holds granular entries (query_properties_compiler.rs:66 filters the rest out), so a match really does mean "the same column under the same alias is already grouped".

Two things worth noting for the general-purpose setter:

  1. This is now a silently dropping add_dimensions, and the other caller is the non-rolling multi-stage path (multi_stage_query_planner.rs:677). There it runs after set_time_dimensions(partition_filter(...)), so ordering is correct — a time dim removed by keep_only/exclude is no longer in time_dimensions and the include re-adds it. That ordering dependency is load-bearing and invisible from here; a one-line note in the doc comment ("callers must apply grain narrowing to time_dimensions before adding") would pin it.

  2. The asymmetry the doc doesn't spell out: a bare grain.include: [returns.day] resolves to returns.day, never to returns.day_day, so it is never deduped against a query granularity — it adds the full-resolution column as an extra key. That's pre-existing grain semantics, but it is the reason the two fixture measures (include: returns.day vs include: returns.day.day) take different code paths here, and the fixture data can't tell them apart (see my note on the seed).

@@ -0,0 +1,16 @@
CREATE TABLE grain_returns (
day DATE NOT NULL,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

day DATE makes grain.include: [returns.day] and grain.include: [returns.day.day] indistinguishable in the data: the raw column and its day truncation are the same value, so both fixture variants exercise the same grouping even though they take different paths through add_dimensions (raw name never dedups against returns.day_day; the qualified one always does).

The reported model almost certainly has a TIMESTAMP. Making this column TIMESTAMP with two different times inside one day would (a) keep the same expected -0.34, since the daily factor is unchanged, and (b) actually distinguish the two grain spellings — the raw include would compute a log return per timestamp and give a different number, which is the semantics a reader of these two fixtures would want confirmed one way or the other.

Comment on lines +13 to +27
fn parse_table(result: &str) -> (Vec<String>, Vec<Vec<String>>) {
let mut lines = result.lines();
let header = lines
.next()
.expect("result has no header")
.split('|')
.map(|c| c.trim().to_string())
.collect::<Vec<_>>();
lines.next();
let rows = lines
.filter(|l| !l.trim().is_empty())
.map(|l| l.split('|').map(|c| c.trim().to_string()).collect())
.collect();
(header, rows)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Third hand-rolled pipe-table parser in this test tree — tests/integration/cubestore/switch_rolling.rs:146 has its own, with a comment about the separator-vs-negative-first-cell ambiguity that this one sidesteps by skipping line 2 positionally. Worth hoisting a shared parse_table (and ideally assert_measures, which is the more useful of the two) into test_fixtures/test_utils so the next value-asserting integration test doesn't write a fourth.

Not blocking — value-based assertions against Postgres are exactly the right choice for this fix, and much stronger than the SQL-shape snapshots these bugs slipped through.

@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.97%. Comparing base (66bba68) to head (91dbf5b).
⚠️ Report is 7 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff             @@
##           master   #11639       +/-   ##
===========================================
+ Coverage   59.34%   79.97%   +20.63%     
===========================================
  Files         228      489      +261     
  Lines       18222   102701    +84479     
  Branches     3675     3676        +1     
===========================================
+ Hits        10814    82138    +71324     
- Misses       6861    20012    +13151     
- Partials      547      551        +4     
Flag Coverage Δ
cube-backend 59.36% <ø> (+0.01%) ⬆️
cubesql 84.42% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The rejection fired on the keys being declared, so it also refused queries the
keys never touch: `exclude` of a member the grain does not carry subtracts
nothing, and `keep_only` listing everything the query groups by intersects to
the same list. Those queries were answered correctly before the guard existed,
and the answer is the one the measure would give with no keys at all.

Reject the narrowing actually happening instead — compare the reshaped grain
with the one it came from, since `partition_filter` only ever removes.

The check also moves below the no-time-dimension branch. There the window has
no frame to build and the measure is planned through the ordinary multi-stage
path, which narrows the grain and broadcasts it back the usual way, so the
combination is supported and no longer refused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @waralexrom's task in 3m 21s —— View job


Re-review of 91dbf5b: guard placement addressed — 0 high, 1 medium (docs, unchanged), 3 low. 2 new inline comments, 3 prior threads still apply.

Full review
  • Diff the new commit against the previously reviewed state
  • Verify the new narrows predicate against partition_filter semantics
  • Check it against how the non-rolling path decides to broadcast (keys_input)
  • Re-check the three rejection tests still hit the moved guard
  • Review the new fixtures and the three new value tests
  • Resolve my now-addressed thread, dedupe against the rest

What changed since the last review

91dbf5b answers the medium I raised: the guard moved below the time_dimensions.is_empty() branch, and it now rejects the narrowing actually happening rather than the keys being declared. My thread on the old placement is resolved.

What I verified

  • The new predicate matches what the non-rolling path already does. any_missing (multi_stage_query_planner.rs:707) compares post-partition_filter new_state against state and requests keys_input only when something was removed — so "a narrowing that removes nothing needs no broadcast side" is the rule the plain path already runs on, not a new claim. partition_filter (:309) only ever filters, so a length comparison is a sound test for it. The two paths now agree, which the previous version did not.
  • keep_only can't widen, so the inert allowance can't hide a grain extension. group_by: [returns.security, returns.day] against a query grouped only by security reads as if it asked for a finer grain, but partition_filter never adds — and the plain multi-stage path doesn't either. The rolling path matching it is correct, not lenient.
  • The three rejection tests still reach the guard. Each grew a granularity: month time dimension; matches_grain_reference (member_symbol.rs:236-238) matches a time dimension against its base, so keep_only: [returns.security] drops day.month from time_dimensions, length shrinks, rejection fires. The message still contains every string the tests assert (grain.exclude, grain.keep_only, reduce_by, group_by).
  • The three new value tests assert the right numbers. With two equally weighted rows per day in January, weight_ytd is 600 pooled / 300 per security, and weight_ytd_reduce_security is 600 in both — inert where the grid has no security, pooling where the no-time-dimension path honours the narrowing. The error message's own suggestion ("query the measure without a time dimension") is now accurate, which it wasn't before.

Medium

  1. Still no docs change. docs-mintlify/reference/data-modeling/measures.mdx:639 documents grain, :696 the group_by/reduce_by migration; neither mentions that narrowing keys next to rolling_window error out. The restriction is narrower after this commit — which makes it harder to explain from the reference, not easier, since whether the model works now depends on the query. A <Warning> in the grain section plus a line in the migration table would cover it, and the release note should say this turns a wrong answer into a hard failure.

Low

  1. The guard clones both dimension vectors on every rolling measure even when no narrowing key is declared; a grain.exclude.is_some() || grain.keep_only.is_some() short-circuit makes it free in the common case. Same comment notes that the guard reads state while the frame above reads self.query_properties — defensible, but worth a word since the divergence is invisible. (inline)

  2. The fixture comment above log_return_sum_ytd_keep_only ("partition-shaping grain keys have no meaning next to a window frame") is stale as of this commit — inert keys and the no-time-dimension case both have a meaning now. (inline)

  3. Three prior threads still apply, unchanged by this commit — the add_dimensions ordering note (query_properties.rs:732), the day DATE seed making the two grain.include spellings indistinguishable, and hoisting parse_table/assert_measures into test_fixtures/test_utils. Left unresolved rather than re-commented.

Coverage gaps (minor, new)

The empty keep_only measure is now accepted without a time dimension, where it collapses the grain to a grand total broadcast onto the rows — the one newly-legal shape with no test. Also untested: narrowing that removes only a non-time dimension while the frame's time dimension survives (keep_only: [returns.day] with dimensions: [security]), which is the rejection path stated in its purest form.

Observation, not a finding

The rejection is now query-shape-dependent: the same measure answers one query and errors on another. That's the correct trade — it's the only way to keep the queries that were already right — and the message says "narrows the grain of this query", which is the right framing. But it does mean the combination can't be caught at model-validation time, and a dashboard can break when a user adds a dimension to a tile. Worth one sentence in the docs note above.

Not verified

No build permissions in this environment, so cargo check, the Rust integration suite, and the JS suite were not run; the review is by reading. The new code compiles as far as I can tell — partition_filter takes &Vec<Rc<MemberSymbol>> and state.dimensions() returns exactly that, and grain is still live at the guard.

· branch [`tesseract-rolling-window-grain-include`](https://github.com/cube-js/cube/tree/tesseract-rolling-window-grain-include)

Comment on lines +888 to +891
let narrows = Self::partition_filter(state.dimensions(), &grain).len()
!= state.dimensions().len()
|| Self::partition_filter(state.time_dimensions(), &grain).len()
!= state.time_dimensions().len();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The predicate is the right one, and for a reason worth recording: the plain multi-stage path decides whether to build the broadcast side with exactly this test — any_missing at line 707 compares new_state (post-partition_filter) against state and only then requests keys_input. So "a narrowing that removes nothing needs no broadcast" is not a new claim here; it's the same rule the non-rolling path already runs on. Rejecting narrows rather than "keys declared" makes the two paths agree, which is stronger than the previous version.

Two small things:

  1. Short-circuit the common case. For every rolling measure with no narrowing keys at all — the overwhelming majority — this still clones both dimension vectors twice to compare lengths. if grain.exclude.is_some() || grain.keep_only.is_some() in front makes the guard free when there is nothing to check, and reads as "only measures that declare narrowing are examined".

  2. state vs self.query_properties. The guard evaluates against state, while the time_dimensions vec that decides the is_empty() branch and the frame above is built from self.query_properties. For a rolling measure nested under a parent multi-stage that already narrowed the grain, those diverge: state.time_dimensions() can be empty while query_properties still has the time dim, so the frame is built but the guard sees nothing to narrow. Checking against state is the more defensible of the two (narrowing is relative to the grid this stage is handed), and the divergence in the frame construction is pre-existing — but the mixed use inside one function is worth a word, since the next reader will assume both sides look at the same list.

sql: "EXP({CUBE.log_return_sum_day_granularity}) - 1"
type: number

# Partition-shaping grain keys have no meaning next to a window frame;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment went stale with the latest commit: partition-shaping keys do have a meaning next to a window now in two cases — when the narrowing is inert on the query's grain (weight_ytd_reduce_security, right below) and when the query carries no time dimension at all (test_rolling_narrowing_without_time_dimension). What this measure demonstrates is narrowing that actually removes a key the query groups by, which is the case with no broadcast side.

Something like "narrowing the grain the value is reported at has no answer next to a window frame — the planner must say so rather than quietly ignoring the key" keeps the intent and stays true of the three rejection fixtures specifically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

javascript Pull requests that update Javascript code rust Pull requests that update Rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant