Repository navigation
feat(stage0, types): memo hash-consing, group properties (column_sign, depends_on_evaluation_time, aggregate_coverage), @ end()/@ start() above their subtree (#1207 PR 2) - #1230
Draft
zzylol wants to merge 1 commit into
Conversation
zzylol
force-pushed
the
refactor/1207-properties
branch
2 times, most recently
from
November 6, 2026 19:12
60a16ee to
107a77b
Compare
zzylol
force-pushed
the
refactor/1207-memo
branch
from
November 6, 2026 19:14
9e7e404 to
7ea9152
Compare
zzylol
force-pushed
the
refactor/1207-properties
branch
2 times, most recently
from
November 6, 2026 21:38
c7a0bc9 to
9e5ce62
Compare
zzylol
force-pushed
the
refactor/1207-memo
branch
from
November 6, 2026 21:38
54d8b30 to
60c5cfe
Compare
zzylol
force-pushed
the
refactor/1207-properties
branch
from
November 6, 2026 22:11
9e5ce62 to
96d3457
Compare
zzylol
force-pushed
the
refactor/1207-memo
branch
from
November 6, 2026 22:11
60c5cfe to
4915a82
Compare
zzylol
force-pushed
the
refactor/1207-properties
branch
from
November 6, 2026 22:46
96d3457 to
b250852
Compare
zzylol
force-pushed
the
refactor/1207-memo
branch
from
November 6, 2026 22:46
4915a82 to
e710906
Compare
This was referenced Nov 6, 2026
zzylol
force-pushed
the
refactor/1207-properties
branch
2 times, most recently
from
November 7, 2026 20:55
7ceb7c3 to
12a4b96
Compare
zzylol
force-pushed
the
refactor/1207-memo
branch
from
November 7, 2026 21:38
e710906 to
ef0e774
Compare
zzylol
force-pushed
the
refactor/1207-properties
branch
from
November 8, 2026 15:10
12a4b96 to
cf93b37
Compare
zzylol
force-pushed
the
refactor/1207-memo
branch
from
November 8, 2026 15:18
30670f8 to
fe0ff33
Compare
zzylol
force-pushed
the
refactor/1207-properties
branch
3 times, most recently
from
November 9, 2026 04:33
3c28259 to
8030d3f
Compare
…, depends_on_evaluation_time, aggregate_coverage), @ end()/@ start() lifted above their subtree (#1207 PR 2) - Memo: hash-consing (find_or_add_group by expression hash), candidate storage (add_candidate), add_query(MemoQuery); Memo::new(declared column signs), private declared_column_signs read by a Scan's column_sign. - Stage 0: memo_of_queries(roots, accuracy_targets, data) adds each query with its accuracy target and n_q (values_per_result over series_read), and collects the declared signs of the scanned sources (a counter's value is non-negative without a declaration). - GroupProperties::column_sign derived per operator (concepts doc, "Value sign"); depends_on_evaluation_time: a TimeRange or time()/now() makes a result depend on the evaluation time, @ T pins it. - aggregate_coverage for summary states, chains, TimeRange/TimeShift over a chain, merges and exact Aggregates; timestamp constraints are IntervalConstraintLimit intervals (t - o - W, t - o]; SQL timestamp literals and identity casts fold so absolute filters lift. - PromQL: @ end() / @ start() are lowered as TimeShift { at } above the selector or subquery they anchor, which is lowered without them, so it is the group of the same expression without @; coverage stops at that TimeShift (it is the definition's input group).
zzylol
force-pushed
the
refactor/1207-memo
branch
from
November 9, 2026 04:40
fe0ff33 to
8095a51
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The concepts doc (section 3) says rules add groups only through$n_q$ values. PR 0 left all of them as
Memo::find_or_add_groupand candidates only throughMemo::add_candidate, and that every group property is derived once, when the group is created, from its input groups. Pass 1 (PR 3) and Pass 2 (PR 4) are built on these methods; Stage 2 prunes Count-Min builds bycolumn_sign(PR 5), PR 1'sdelivers_onreadsdepends_on_evaluation_time, and Pass 2 splits a query's failure probability over itstodo!(), so planning stopped at Stage 0.@ end()/@ start()pin a subtree to one end of the request. Lowered in place, the pinned selector was a group of its own and shared nothing with the same selector without@(#1251). The approved design (2026-11-08) lifts the@above the subtree, so the subtree is the ordinary group.What
types/src/stages/candidates.rs):find_or_add_group(expression)hash-conses on the expression (operator, arguments, input group ids) and derivesGroupPropertiesonce withderive_group_properties; an expression that breaks the node rules returns the error and changes nothing. An expression whose copies may return different rows (an unorderedLIMIT) always gets a new group.add_candidate(group, candidate)derives the candidate's properties the same way, requires its schema and result kind to equal the group's, drops a duplicate (same expression, same θ), and keepsparentsandqueriesup to date.add_query(MemoQuery)lists the query on every group its root reaches.Memo::new(declared_column_signs)(new): the memo holds the signs the data workload declares, which aScan'scolumn_signreads (P6, the recommendation; private fielddeclared_column_signs).stage0-memo):memo_of_queries(roots, accuracy_targets, data)builds the groups bottom-up, collects the declared signs of the sources the queries scan (SourceFacts::column_signs; a declared counter'svalueis non-negative without a declaration), and adds each query as aMemoQuerywith its accuracy target andvalues_per_result(root, series_read(root, data), data)(Nonewhendatabounds nothing).GroupProperties::column_sign(types/src/stages/column_sign.rs, new module): per output column, per the doc's "Value sign" table. Scan: declared sign, elseUnknown; Filter/TimeRange/TimeShift/Sort/Limit/Dedup/column Project: the input's;absNonNegative, negation flips, numeric literals and casts; sum/avg/min/max/quantile/top-k/first/last keep the value's sign; counts and spreadsNonNegative;rate/irate/increaseNonNegativeover aNonNegativeinput; a state records the sign of its update values; a merge keeps the sign its inputs agree on; a readout reads the state's (a distinct count, L2 norm or entropy isNonNegative); anything elseUnknown.GroupProperties::depends_on_evaluation_time: true for aTimeRangeand for a scalar expression readingtime()/now(); false forTimeShift { at: Some(Timestamp(T)) }(pinned); otherwise true when an input group's is.aggregate_coveragefor summary states, chains,TimeRange/TimeShiftover a chain, merges and exactAggregates. Time is an interval on the timestamp column withIntervalConstraintLimitends, left-open and right-closed:m[5m] offset 1h→(t − 65m, t − 60m],@ T→ fixed timestamps. SQL folds a timestamp cast of a literal and resolution drops a cast to the operand's own type, sots >= '2026-11-01'lifts.SummaryMerge(TimeRange(TimeShift(Chain[w](X))))covers whatSummaryAgg(TimeRange(TimeShift(X)))does, andadd_candidateaccepts it in that group. Counter states merge only over intervals disjoint in time.@ end()/@ start()(frontend-promql/src/promql.rs): the selector or subquery X is lowered without the@(its offset and an@ Tstay in X), andTimeShift { offset_ms: 0, at: Some(End | Start) }is put above it. Instant and range-evaluated queries lower alike;PromqlLowerer::range_evaluated(the previous revision's bool) is gone. The coverage walk stops at thatTimeShift: it is the definition'sinput_group.docs/memo-group-model, 10e5852):Memo::newanddeclared_column_signs; the "Value sign" rows for counts, rates, comparisons and readouts;depends_on_evaluation_timecoverstime()/now().asap-primitive-schema.md§4.2.2 (coverage over memo groups, the time limits table, the@rows).Decisions
Result; one set of node rules for groups and candidatesAggregatecoverage, time as an interval withIntervalConstraintLimit,@ Tfixed, SQL timestamp filters,DisjointInTime, chain coverage and the merge over a chain's windows(t − r, t]@ end()/@ start()lifted above the anchored subtree asTimeShift { at }(X), X without@;range_evaluateddeleted; coverage leaves theTimeShift(@)unliftedValueSign/column_signderived per operator, Scan from declared source factsdepends_on_evaluation_timederived in Stage 0Memo::new, privatedeclared_column_signs)MemoQuery::accuracy_target: Option<_>(P17),memo_of_queries(roots, accuracy_targets, data)(P5)Pending (new here, not in the doc before this PR)
SourceColumn { source, column }inir/operator/operator_properties.rs, besideSourcedeclared_column_signs. The doc defines it under "Column lineage"; PR 2b (lineage) will use the same type forColumnLineage::SourceColumnand the selection keys, and deleteColumnIdentity.first/last_over_time; counts and spreads (count_values,changes,resets,group,absent,present_over_time,stddev,stdvar, L2 norm, entropy)NonNegative;rate/irate/increaseNonNegativeonly over aNonNegativeinput; vector comparison/and/unlesskeep the left value's sign,orthe agreed sign,boolcomparisonsNonNegative; a distinct count, L2 norm or entropy readout and an exact count's finalizationNonNegativeNonNegative" and "readouts: the state's recorded sign". A rate over values of either sign can be negative (a decrease reads as a reset: −5 then −10 gives −10), and a distinct count over negative values is still non-negative, so the literal rows would derive wrong signs. Doc updated (10e5852).depends_on_evaluation_timeis true fortime()/now()TimeRangeand@ T; a filterts > now()reads the evaluation time too.@lifted above the selector only, not above the functions over itrate(m[5m] @ end())lowers torate(TimeShift(@ end())(m[5m])): the doc's "the subtreem[5m]". Lifting further (TimeShift(@ end())(rate(m[5m])), equal while nothing above reads the evaluation time) would also share theratewith instant queries; not done.How
find_or_add_groupderives the properties, hashes the operator's arguments together with the input group ids (hash_consing::expression_hash), and compares the bucket withsame_expression. Only shareable expressions are indexed.add_candidatechecks before changing anything, then appends, adds parents, and walks down from the inputs inserting the group's queries in sorted position (it stops below a group that already had them).group_ofmaps each node by address to its group (a diamond is visited once), children first.declared_column_signswalks the roots once for the scanned sources and readsDataWorkload::series_facts, so a histogram's_sum/_countseries get the family's declarations and the counter rule.output_column_sign(operator, output, declared)follows each operator's output layout (aggregate_output_schema's for aggregates and states,chain_schema's for a chain:window_endUnknown, the build's own time column dropped; a merge over a chain's windows reads the chain's build). A layout it does not know (a readout whose output does not keep the state's columns) isUnknowneverywhere.debug_assertchecks one sign per output column.time_constraintsums the offsets of theTimeShifts on the path, takes the one range width and the one@ T, and returns the interval; the walk inof_aggregatedoes not pass aTimeShift { at: End | Start }.Key code interfaces added / modified
types/src/stages/candidates.rs,types/src/stages/memo.rstypes/src/ir/operator/operator_properties.rsstage0-memo/src/lib.rsPromQL lowering has no interface change; its output shape for
@ end()/@ start()changes (TimeShift { at }above the selector).Usage
The workload
sum by (host) (rate(requests_total[1m]))andtopk(5, sum by (host) (gauge))over data declaringrequests_totala counter and nothing forgauge(Stage 0 testsa_scan_reads_the_declared_column_signs,each_query_enters_with_the_values_one_evaluation_returns; types testa_top_k_ranks_values_of_its_inputs_sign):And
@ end()(lib testat_start_and_at_end_are_lifted_above_the_selector_they_anchor, types testat_end_and_at_start_share_the_subtree_they_anchor_and_stay_unlifted):Major algorithm changes
Stage 0 hash-consing.
cse::same_node, deleted in PR 0) and the group lookup the rules need were two equalities that could disagree (on a guarantee, for one).Rctable, then numbered and paired with groups.group_ofcallsMemo::find_or_add_group(operator over child groups)bottom-up. 2.add_queryper root, with its target andmemo_tests10.O(arguments)per lookup (children are ids);O(N)for N nodes; one extra walk over the roots for the scanned sources.Aggregate coverage with time intervals.
todo!();@ Tand SQL timestamp filters had no representation; exact aggregates had no coverage.TimeRangegave a separate relative time field; a chain expanded to a build over count × width.time_constraintgives(anchor − o − W, anchor − o]on the timestamp column; a chain is a build over its input with time unconstrained, and a window selection over it adds the interval;DisjointInTimecompares the timestamp column; an exactAggregateis a top unlifted operator; the walk stops atTimeShift { at: End | Start }.TimeRange's window iff its timestamp is in the interval; equal coverage means equal rows. Tests:memo_summary_coverage.rs(13).TimeRanges or@ Ts.Value sign and evaluation-time dependence: new derivations, one pass per group over its operator and its input groups' stored properties (
O(output columns)). Tests:memo_group_properties.rs(5), Stage 0'sa_scan_reads_the_declared_column_signs.Before this PR
On PR 1,
memo_of_queries,find_or_add_group,add_candidateandadd_querypanic attodo!(), so every planning test stops at Stage 0.m[5m] @ end()lowered asTimeRange(TimeShift(@ end())(Scan))in a range query and asm[5m]in an instant one, through arange_evaluatedflag.After this PR
sum by (host) (rate(requests_total[1m])), a counterNonNegative,depends_on_evaluation_timetruesum by (host) (gauge), nothing declaredUnknownlatencydeclared{"value": "non_negative"},quantile_over_time(0.99, latency[1m])NonNegativem[5m] @ 1700000000depends_on_evaluation_timefalse; coveragets ∈ (1699999700000, 1700000000000]latency[5m] @ end()TimeShift(@ end())over the group oflatency[5m], shared with instant queriesmwith 1000 declared seriesMemoQuery::values_per_evaluation == Some(1000)Planning passes Stage 0 and stops at Pass 1's target discovery (PR 3).
Deleted, and what replaces each
PromqlLowerer::range_evaluatedand therange_evaluatedparameter oflower_query,vs_parts,time_shift,extract_matrix(previous revision)anchored/selectorinpromql.rs:@ end()/@ start()above the subtree, the same for every queryat_end_and_at_start_share_coverage_only_in_an_instant_query(types)at_end_and_at_start_share_the_subtree_they_anchor_and_stay_unliftedat_start_and_at_end_stay_only_in_a_range_query(promql lib)at_start_and_at_end_are_lifted_above_the_selector_they_anchordiffering_accuracy_targets_are_two_groups_over_one_scan_group(measures carry no target since PR 0)queries_that_differ_only_in_their_accuracy_target_are_one_grouptodo!()s forcolumn_sign,depends_on_evaluation_time, Stage 0,add_query, and#[allow(unreachable_code)]onderive_group_propertiesSelf-review: over-abstraction, duplication, dead code
Memo::newand the privatedeclared_column_signs(P6, approved),SourceColumn(Q1, the doc's type), privatecolumn_sign.rsfunctions,MemoGroupAsInput::group_properties()(pub(crate), one accessor the two derivations share). No trait or wrapper added; the sign derivation is not generic overOperatorInputbecause only memo groups have signs.column_sign.rsrepeats the output-column layouts ofaggregate_output_schemaandchain_schema(keys, then measures; a per-entity reduction in place). PR 2b'scolumn_lineagederives the same layouts; when PR 2b is rebased it should map both properties through one layout function instead of a second copy (noted in the PR 2b handoff).MemokeepsDefault(an empty memo without declared signs, used by tests and byMemo::new) besideMemo::new.-D warnings; therange_evaluatedplumbing is gone; the stale#1207 PR 2comments are removed.TimeShift { at: End | Start }and returned no interval for the whole path; it now stops there, so the time path below it stays the anchored group's own.Verification
cargo test -p <crate> --all-features --no-fail-fastat the head (8095a51). Every failure panics at a later PR'stodo!()(or is the stale fixture, PR 9); planning now stops at Pass 1's target discovery (PR 3).asap_plannersubprocess that stops at atodo!(): 2; label a group by its expression (PR 9): 1Compared by test name with PR 1 at
8030d3fe6: no test that passed fails. 45 tests are new and pass (asap-types30,asap-stage0-memo15, one each inasap-frontend-promqlandasap-frontend-sql); PR 1'sdelivers_on_a_chain_when_its_width_divides_the_context_timesanddelivers_on_a_stored_result_on_its_group_gridfailed at this PR'stodo!()and pass. Tests that failed at PR 2'stodo!()on PR 1 now fail at PR 3's (target discovery) or PR 6's.cargo clippy --workspace --all-targets --all-features -- -D warnings: clean.cargo fmt --all --check: clean.crate_graph: 6 passed.memo_group_properties.rs(5: sign table rows for row operators, aggregates, states/merges/readouts, the doc's top-k example;depends_on_evaluation_timetrue/false),at_end_and_at_start_share_the_subtree_they_anchor_and_stay_unlifted, Stage 0'seach_query_enters_with_the_values_one_evaluation_returns,a_scan_reads_the_declared_column_signs,queries_that_differ_only_in_their_accuracy_target_are_one_group, promql libat_start_and_at_end_are_lifted_above_the_selector_they_anchor, conformanceat_modifier_lowers_to_a_time_shift(updated).8030d3fe6(previous head kept asbackup/1207-memo-fe0ff3357); the four previous commits are squashed into one.