Repository navigation
feat(ir): add logical sub-DAG sharing and export without execution timing - #537
Merged
Merged
Conversation
This was referenced Oct 2, 2026
zzylol
force-pushed
the
stack/528-02-ir
branch
from
October 2, 2026 18:32
a59aff8 to
8f384f7
Compare
zzylol
force-pushed
the
stack/528-03-graph
branch
from
October 2, 2026 18:32
9369217 to
9870df6
Compare
zzylol
force-pushed
the
stack/528-02-ir
branch
from
October 2, 2026 19:40
8f384f7 to
d8f0b1a
Compare
zzylol
force-pushed
the
stack/528-03-graph
branch
from
October 2, 2026 19:40
9870df6 to
88c2931
Compare
zzylol
force-pushed
the
stack/528-02-ir
branch
from
October 2, 2026 21:14
d8f0b1a to
dd71af0
Compare
zzylol
force-pushed
the
stack/528-03-graph
branch
2 times, most recently
from
October 2, 2026 21:22
c0638f6 to
d414efd
Compare
zzylol
force-pushed
the
stack/528-02-ir
branch
from
October 2, 2026 21:22
dd71af0 to
7d5c382
Compare
zzylol
force-pushed
the
stack/528-03-graph
branch
from
October 2, 2026 21:25
d414efd to
ec0417b
Compare
zzylol
force-pushed
the
stack/528-02-ir
branch
from
October 2, 2026 21:25
7d5c382 to
6aba8f2
Compare
zzylol
force-pushed
the
stack/528-03-graph
branch
3 times, most recently
from
October 3, 2026 02:39
00a3d27 to
467baad
Compare
zzylol
force-pushed
the
stack/528-03-graph
branch
from
October 3, 2026 14:57
467baad to
aecd114
Compare
This was referenced Oct 3, 2026
zzylol
force-pushed
the
stack/528-02b-merge-structure
branch
from
October 3, 2026 16:07
ba26c8b to
7cd4ee0
Compare
zzylol
force-pushed
the
stack/528-03-graph
branch
3 times, most recently
from
October 3, 2026 16:33
76b23bc to
c30977a
Compare
zzylol
added a commit
that referenced
this pull request
Oct 6, 2026
Follow #537: the remaining tests read Operator<NodeId> payloads from the physical DAG, and docs name ir::flat / ir::physical_export instead of the removed ir::export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
marked this pull request as ready for review
October 6, 2026 18:06
Contributor
Author
|
Thanks for the review. Addressed in 4ac03c3:
|
zzylol
added a commit
that referenced
this pull request
Oct 6, 2026
Follow #537 and #541: the physical DAG carries Operator<NodeId> instead of the removed wire mirror types. Port the moved physical_planner, the planner's lifecycle export and the tests that read or hand-build physical DAGs; hand-built nodes now name their children by id, matching their edges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
force-pushed
the
stack/528-03-graph
branch
from
October 6, 2026 20:02
4ac03c3 to
ebea327
Compare
zzylol
force-pushed
the
stack/528-02b-merge-structure
branch
from
October 6, 2026 20:02
f2b7b3f to
3f5d369
Compare
zzylol
added a commit
that referenced
this pull request
Oct 6, 2026
Follow #537: the remaining tests read Operator<NodeId> payloads from the physical DAG, and docs name ir::flat / ir::physical_export instead of the removed ir::export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
added a commit
that referenced
this pull request
Oct 6, 2026
Follow #537: the remaining tests read Operator<NodeId> payloads from the physical DAG, and docs name ir::flat / ir::physical_export instead of the removed ir::export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
force-pushed
the
stack/528-02b-merge-structure
branch
from
October 6, 2026 20:17
3f5d369 to
52b4003
Compare
zzylol
force-pushed
the
stack/528-03-graph
branch
from
October 6, 2026 20:17
ebea327 to
a419b14
Compare
This was referenced Oct 6, 2026
Contributor
Author
|
This PR shows as merged, but only into its old stack base |
zzylol
added a commit
that referenced
this pull request
Oct 6, 2026
Follow #537: the remaining tests read Operator<NodeId> payloads from the physical DAG, and docs name ir::flat / ir::physical_export instead of the removed ir::export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
added a commit
that referenced
this pull request
Oct 6, 2026
Follow #537: the remaining tests read Operator<NodeId> payloads from the physical DAG, and docs name ir::flat / ir::physical_export instead of the removed ir::export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
added a commit
that referenced
this pull request
Oct 7, 2026
…SAP DAG Follow #537: the physical DAG now carries each node's operator as Operator<NodeId> (from ir::flat) instead of the removed wire mirror types. The runtime matches Operator::NonASAP / Operator::ASAP directly, and restore() rebuilds the in-memory DAG with map_children instead of a field-by-field conversion. Node ids are plain usize. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
added a commit
that referenced
this pull request
Oct 7, 2026
Follow #537 and #541: the physical DAG carries Operator<NodeId> instead of the removed wire mirror types. Port the moved physical_planner, the planner's lifecycle export and the tests that read or hand-build physical DAGs; hand-built nodes now name their children by id, matching their edges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
added a commit
that referenced
this pull request
Oct 7, 2026
Follow #537: the remaining tests read Operator<NodeId> payloads from the physical DAG, and docs name ir::flat / ir::physical_export instead of the removed ir::export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
added a commit
that referenced
this pull request
Oct 12, 2026
…SAP DAG Follow #537: the physical DAG now carries each node's operator as Operator<NodeId> (from ir::flat) instead of the removed wire mirror types. The runtime matches Operator::NonASAP / Operator::ASAP directly, and restore() rebuilds the in-memory DAG with map_children instead of a field-by-field conversion. Node ids are plain usize. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
added a commit
that referenced
this pull request
Oct 12, 2026
…SAP DAG Follow #537: the physical DAG now carries each node's operator as Operator<NodeId> (from ir::flat) instead of the removed wire mirror types. The runtime matches Operator::NonASAP / Operator::ASAP directly, and restore() rebuilds the in-memory DAG with map_children instead of a field-by-field conversion. Node ids are plain usize. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
added a commit
that referenced
this pull request
Oct 12, 2026
…SAP DAG Follow #537: the physical DAG now carries each node's operator as Operator<NodeId> (from ir::flat) instead of the removed wire mirror types. The runtime matches Operator::NonASAP / Operator::ASAP directly, and restore() rebuilds the in-memory DAG with map_children instead of a field-by-field conversion. Node ids are plain usize. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
added a commit
that referenced
this pull request
Oct 12, 2026
…#541) * feat(ir): restore execution timing assignment for physical plans The logical export is phase-free. Physical planning still needs the lifecycle timing expansion; bring it back unchanged except for carrying coverage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ir): export timed plans as PhysicalASAPDAG The logical export carries no execution timing. Physical planning output needs it: reuse the logical payloads and node ids, and add each node's and edge's data state plus window compatibility, as the earlier post-ASAP export did. Coverage is carried through. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(runtime): compile unified operator and scalar graphs * feat(ir): give physical ASAP DAGs one root per batch query Match the logical export: a batch is one DAG whose roots are its queries, with shared sub-DAGs exported once. The runtime compiles all roots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: integrate with #614 (DataFusion 54): let chrono resolve to DF 54's version DataFusion 54 requires chrono ^0.4.44, so the runtime crate's exact =0.4.39 pin no longer resolves; keep 0.4.39 as the minimum. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(runtime): read Operator<NodeId> payloads from the physical ASAP DAG Follow #537: the physical DAG now carries each node's operator as Operator<NodeId> (from ir::flat) instead of the removed wire mirror types. The runtime matches Operator::NonASAP / Operator::ASAP directly, and restore() rebuilds the in-memory DAG with map_children instead of a field-by-field conversion. Node ids are plain usize. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(ir): follow derived summary coverage in timing and physical export Coverage is derived from the node (#646), so timed copies no longer carry it and PhysicalASAPDAGNode drops its coverage field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(runtime): address #541 review findings - timing: a SummaryAgg the lifecycle assignment recomputes at query time now forces the SummaryAgg consuming it to query time (`forces_query_time` reads the assignment), instead of failing with EvaluationUnderMaintenance. - physical export: `PhysicalASAPDAG::validate` requires each node's incoming edges to be exactly its operator's inputs with their roles, so swapped Left/Right edges cannot reorder `a - b` (EdgeRoleMismatch). - physical export: a DAG that fails validation (an empty batch, a hand-timed plan) is an `InvalidPhysicalDAG` error instead of a panic. - precompute: only a TimeRange over (a TimeShift over) a time series Scan takes the raw-sample input contract, as before the port; a table range needs a stored population. - compile: an input binding below a node the PromQL fallback evaluates is an error; the fallback reads raw series slots, so it was silently ignored. A binding below another bound input is still superseded by it, as in `cut`. - compile: drop the scalar-literal binary branch; nothing filled its map. - PromQL fallback: clamp's `NOT (max < min)` is now the IR `Filter` #540 emits; the fallback runs a filter on scalars alone, and the pointwise projection no longer filters on its own. - tests: `raw_series_contract_is_explicit` asserts that a consumed bare selector is instant selection (its comment said the opposite) and that a binding below the fallback is rejected; new `unified_precompute` test. - PromQL fallback: unary minus drops the metric name like every other pointwise projection, following #540 and the Prometheus 3.15 baseline (#511); pointwise_projection_names_and_dynamic_parameters expects it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * refactor(runtime): remove the PromQL fallback evaluator ASAPPlanner generates candidates and computes their cost, or says why a query has none. Executing what the native operators cannot run belongs to the deployment runtime (ASAPQuery backend), not to the Planner. Remove unified_physical_planner::promql_fallback and its tests, and the fallback paths in compile: raw-series input slots, the fallback lowering and the below-fallback binding check. A node without a native lowering now fails compilation with Error::Invalid. Reject Planner node IDs above u32::MAX, which could collide with helper IDs, and error instead of debug-asserting when a node needs more than 2^16 helpers. Document that forces_query_time also forces an unaligned per-series PromQL BinaryOp to query time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
added a commit
that referenced
this pull request
Oct 12, 2026
Follow #537: the remaining tests read Operator<NodeId> payloads from the physical DAG, and docs name ir::flat / ir::physical_export instead of the removed ir::export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
Follow #537: the remaining tests read Operator<NodeId> payloads from the physical DAG, and docs name ir::flat / ir::physical_export instead of the removed ir::export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
Follow #537: the remaining tests read Operator<NodeId> payloads from the physical DAG, and docs name ir::flat / ir::physical_export instead of the removed ir::export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
Follow #537: the remaining tests read Operator<NodeId> payloads from the physical DAG, and docs name ir::flat / ir::physical_export instead of the removed ir::export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
…ation options (#667) * refactor(planner): remove the summary maintenance lifecycle Stage 2 materialization (#509) will decide per sub-DAG whether and when to materialize, so the Planner no longer chooses a maintenance lifecycle. - MajorPass now runs search_workload_with_targets -> global_selection -> assemble_selected_dag per root -> share_common_sub_dags. - QueryLifecyclePlan becomes QueryPlan { entry_index, root }; PlanOutput's execution_timed_dag times the roots directly. LifecycleInput, the lifecycle errors and UserInput's `lifecycle` field are gone (public API break). - Delete summary_maintenance_lifecycle, summary_maintenance_cost, summary_maintenance_dag_export, post_asap::{summary_maintenance, summary_maintenance_lifecycle} and SummaryWindowFramework (the pane primitives stay), the lifecycle CostModel hooks, CandidateCostOverrides and EmpiricalEvidenceProvider::lifecycle_cost_inputs. - Delete the lifecycle e2e test and the viewer's lifecycle-plan UI; rewire e2e_plan, summary_sharing, operator_design_examples and weighted_topk_binding to the new pipeline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(ir): default to query-time materialization Rename LifecycleAssignment to MaterializationAssignment and apply_lifecycle_timings to apply_materialization_timings. The default assignment is now all_query_time(): nothing is materialized until Stage 2 materialization (#509) decides per sub-DAG. all_ingestion_time() and set() assign maintenance explicitly. - validate_default becomes validate_maintained: candidate legality is still checked with every summary maintained, so candidate generation is unchanged. planned_data_state and fixed_window_rate_candidates use the same maintained assumption, and the maintained precompute compilers in promql_rows assign ingestion time explicitly. - PlanOutput::execution_timed_dag, show_post_asap_ir and the default test helpers now emit query-time summaries. - forces_query_time (#541) treats a SummaryAgg as query time when the assignment, including its default, puts it there. - Tests that exercise maintenance assign it explicitly (new `maintained` helpers); a new unit test pins the query-time default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: drop the summary maintenance lifecycle and document the query-time default Remove the lifecycle APIs, recipes and viewer section from the docs, delete the workload-demand-and-summary-lifecycle proposal, and point materialization questions to Stage 2 (#509). Rename LifecycleAssignment/apply_lifecycle_timings/ validate_default to their new names and describe the all-query-time default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor: finish the Operator<NodeId> port after the legacy cleanup Follow #537: the remaining tests read Operator<NodeId> payloads from the physical DAG, and docs name ir::flat / ir::physical_export instead of the removed ir::export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(ir): materialization per #509: NotMaterialized, MatAtIngestion, MatAtQuery options. Replace the two-way timing assignment with them: - New Materialization { NotMaterialized, MatAtIngestion(Medium), MatAtQuery(Medium) } and Medium { Memory, Disk }; only MatAtIngestion runs at ingestion time. - MaterializationAssignment maps a SummaryAgg or MaintainPopulation to its option (set); Default is NotMaterialized everywhere, and with_default gives unassigned sub-DAGs an option where the DAG allows it (validate_maintained, planned_data_state and the maintained precompute compilers use MatAtIngestion). all_query_time, all_ingestion_time and summary_timing are gone. - MaintainPopulation follows its assignment like SummaryAgg instead of always running at ingestion time. - An explicit MatAtIngestion over something that only exists at query time fails with UpstreamNotAtIngestion ("upstream of ingestion-time node not at ingestion time") instead of silently moving to query time. - OperatorNode and PhysicalASAPDAGNode carry their Materialization; PhysicalASAPDAG validation rejects a materialization inconsistent with the node's timing. - Remove PhysicalASAPDAG::with_execution_phases (#509 Assumption 8: the deployment executes the plan as given). Tests place summaries through the assignment instead. Retention and choosing the option per sub-DAG stay with Stage 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(ir): materialize any node; keep explicit assignments across a split Per #509 ("Materialization decides, for each sub-DAG, whether its output is stored"; Example 4 stores panes), `MaterializationAssignment::set` accepts any node. `MatAtIngestion` puts it at ingestion time, or fails with `UpstreamNotAtIngestion` when something below can only run at query time, and a query-time consumer reads its stored output. An ingestion-pinned node (e.g. a fixed-window rate finalize) over a query-time state now fails the same way instead of with `IllegalChildDataState`. `split_shared_by_phase` returns the assignment remapped onto the rebuilt nodes, so an explicit `MatAtIngestion` is no longer silently lost, and the assignment holds `Rc` clones of its keys so a reused address cannot match a stale entry. Renames the remaining "maintained" names to the docs' vocabulary (`validate_at_ingestion`, `at_ingestion` test helpers), notes that `frontier_from_timing` does not store `MatAtQuery` outputs yet (Stage 2), and bumps PHYSICAL_ASAP_DAG_WIRE_VERSION to 10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * test(integration): same answers with and without materialization Ports the four execution tests removed with the summary-maintenance lifecycle to MaterializationAssignment: NotMaterialized and MatAtIngestion placements of the KLL quantile, the current-series population, and sum_over_time arithmetic over one and over different selectors execute to the same results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(ir): honor explicit materialization on fixed-timing nodes; allow stored roots A node whose timing its candidate fixed keeps an agreeing explicit assignment and rejects a conflicting one (MaterializationConflictsWithFixedTiming) instead of dropping it. A root assigned MatAtIngestion is valid; only an ingestion-time root of rows that is not stored there errors (UnstoredIngestionRoot, was MaintenanceRowsAtRoot). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
Rebased on main d4869a7 (DF 54). Revised after review: the logical export and the
wire.rsmirror types are removed (see item 3).Problem
#536 defined the unified
ir::OperatorNodeDAG, and #567 and #560 added summary coverage andSummaryMergeto it. But canonicalization, common sub-DAG sharing (CSE) and a flat, serializable form have not been implemented on these new types. They exist only for the old split IRs:pre_asap::canonicalize::canonicalize(QueryExpr) -> QueryExprQueryExprpre_asap::cse::share_common_sub_dags(Vec<(Id, QueryExpr)>)QueryExprpost_asap::cseSummaryNodedag_export::export/export_post_asapQueryExpr(+ post-ASAP substitutions)So, on
OperatorNode:Two equivalent spellings stay different shapes. Example:
Filter { EXISTS (s) }andJoin { Semi, true, left, s }mean the same thing but cannot be matched structurally.Two identical sub-DAGs stay two
Rcs. Example: one query with the same grouped p50 on both sides of a comparison:Nothing collapses A and B into one node with two consumers.
A shared DAG cannot be written out flat.
OperatorNodeserializes as a tree, so every shared sub-DAG would be repeated. The earlier version of this PR fixed that with a second copy of the operator and scalar types (wire.rs:NonASAPOpKind,WireScalarExpr, ...) whose children were ids.Therefore, this PR adds:
ir::canonicalize: rewrites equivalent spellings into one shape (fixes 1).ir::cse: shares structurally identical sub-DAGs across a workload batch (fixes 2). This is the identical-expression rule of docs: propose workload-wide planning, summary sharing, and materialization #509 Pass 2: "The input and computation semantics are identical" → "One common computation node serving multiple consumers."Operator,NonASAPOp,ASAPOp,ScalarExpr,Predicate,ProjectItem,SortKeyandQueryRoottake the child reference as a type parameter, defaulting toRc<OperatorNode>.ir::flat::flattenwrites a DAG as a list of nodes whose operators areOperator<NodeId>. No logical export document is added: the planner's stages passRc<OperatorNode>DAGs directly, and the physical DAG (feat(runtime): export timed plans as PhysicalASAPDAG and compile them #541) is the first consumer of the flat form.It does not implement the summary-capability rule or the window-composition rule of #509 Pass 2, Pass 1 candidate generation, or any physical-stage decision (materialization, timing assignment, phase splitting).
Proposed method
All on bound
OperatorNodeDAGs incrates/types/src/ir/.1. Canonicalize (
ir::canonicalize). Bottom-up over every child (operator inputs and nodes read by scalar expressions), memoized byRcpointer so a shared sub-DAG is rewritten once. At each node it applies local rules until none matches:Limit { n: k, offset: 0, Sort { one descending column key, [passthrough Project] Aggregate(Reduce(by), [Count | Sum]) } }becomesAggregate(Reduce(partition), [TopK { k }])over the unchanged inner aggregate. It is skipped for ascending sorts, offsets, ranking by a group key, a Limit partition that differs from the Sort partition, andSumover a derived childAggregate(rate/increase).Filter, oneEXISTS (s)conjunct becomesJoin { Semi, true },NOT EXISTS (s)becomesJoin { Anti, true }, and a positivex IN (s)becomesJoin { Semi, x = Column(left_width) }. Remaining conjuncts stay in an outerFilter. One conjunct per round.NOT IN, anINwhose probe contains a scalar subquery, scalar subqueries (a cross join would lose their zero-row NULL and multi-row error semantics), andROW_NUMBER()filters (removing the window column would change the visible schema).Untouched sub-DAGs keep their pointer identity, a rewritten shared sub-DAG stays shared, and the pass is idempotent. A
Concatwhose first branch changes schema drops itsdiscriminator_unique_key.2. Share common sub-DAGs (
ir::cse). Hash-consing over all roots of a workload batch, bottom-up:PromqlScalarFromVector,ScalarSubquery,Exists,InSubquery). Soscalar(v)in two queries can sharev.structural_hashonly picks a bucket. The decision is typed equality (same_node): children by pointer (or memoized value comparison), thenresult_kind,schema,timing,coverage,guarantee, and the operator's own fields (operator.map_children(|_| ()), so children never enter the comparison twice). Guarantee and own fields must also serialize identically, so0.0vs-0.0andNaNvsNaNare not shared.schema.has_unique_key(). ASAP nodes have no such gate.coverageis part of both hash and equality, so two summary states over different observations are never shared.PartialEq-equal to its input); only itsRcstructure may now alias.The pass does not decide whether sharing is cheaper. That is selection's job (#509 stage 3).
3. Generic child references and
ir::flat. Each operator and scalar type gets a parameterCfor its child reference, defaulting toRc<OperatorNode>, so existing code is unchanged. The traversal helpers (children,scalar_exprs,operator_refs,kind_name) work for anyC;map_children/map_operator_refsmapCto a new typeD. Schema derivation and validation stay on the defaultRc<OperatorNode>form.flatten(roots)visits the DAG children-first and gives each distinctRc(by pointer) the next index. Each node becomes aFlatNode: the same fields asOperatorNode, withoperator: Operator<NodeId>. A scalar root stays aQueryRoot::Scalar(ScalarExpr<NodeId>).OperatorNodeitself is not generic: a self-referential default (OperatorNode<C = Rc<OperatorNode>>) is rejected by rustc (E0391).Key code interfaces
crates/types/src/ir/node.rs,non_asap.rs,asap.rs,scalar.rs,query.rscrates/types/src/ir/flat.rscrates/types/src/ir/canonicalize.rs,cse.rsUsage:
Examples
Example 1: a shared node is flattened once (
crates/types/src/ir/flat.rstests)Input: two roots
Filter(true), each over the sameValuesRc.flattenreturns 3 nodes:0 = Values,1 = Filter { child: 0 },2 = Filter { child: 0 }, and roots[Operator(1), Operator(2)](a_shared_node_is_flattened_once). AProjectwhosechildandScalarSubqueryare the sameValuesgives 2 nodes, withchild: 0andcols[0].expr == ScalarSubquery(0)(scalar_references_become_node_ids). A constant scalar root gives 0 nodes (a_constant_scalar_root_has_no_nodes), and the JSON round-trips with everytiming = None(json_round_trips).Example 2: CSE on the identical-expression rule (
crates/types/src/ir/cse.rstests)The
BinaryOpfrom the Problem section, aftershare_common_sub_dags(vec![("q", root)]):lhsandrhsare the sameRc(single_query_shares_its_own_repeated_sub_dag). The grouped aggregate has unique keyby [1], so it is legal to share.Quantile q=0.5 col=2in two roots (median_and_explicit_half_percentile_merge)col=2vscol=3(distinct_column_quantiles_do_not_merge)no_unique_keys_means_no_merge_even_when_structurally_identical)without(..)aggregate (group_keys_gate_still_prevented_when_partition_by_without_used)Dedup { cols: [1] }(dedup_gates_sharing_the_same_as_aggregate)Dedupadds a unique keyvector(scalar(sum by (service)(up)))twice (scalar_referenced_vector_is_shared_across_queries)SummaryAgg,alpha=0.01, same guarantee (asap_nodes_share_without_a_unique_key)alpha=0.01vs0.001; exact guarantee vsNone(asap_nodes_with_distinct_parameters_or_guarantees_are_not_shared)SummaryEstimatep95 and p99 over equal producers (evaluations_share_their_producer_but_not_each_other)q0.0vs-0.0,+infvs-inf,NaNvsNaN(signed_zero_and_nonfinite_values_remain_distinct)+infvs+infis sharedcoveragecoverageis in hash and equality (no dedicated test)Example 3: canonicalization (
crates/types/src/ir/canonicalize.rstests)Limit 5 { Sort desc col 1 { Aggregate(by [1], [Count]) } }Aggregate(by [], [TopK{k:5}]) { Aggregate(by [1], [Count]) }(promotes_count_ranked_limit_sort); also through a passthroughProjectFilter { EXISTS (s) }Join { Semi, true, left, s }(exists_filter_becomes_semi_join)Filter { NOT EXISTS (s) }Join { Anti, true, left, s }Filter { service IN (s) }over a 3-column leftJoin { Semi, service = Column(3) }Filter { value > 1 AND EXISTS (s) }Filter { value > 1 } { Join { Semi } }Filter { NOT IN (s) }Project [ScalarSubquery(s) AS a, ScalarSubquery(s) AS b]s(scalar_subqueries_remain_explicit_and_shared)Filter { rn <= 5 } { ROW_NUMBER window }Every rewrite test also checks
canonicalize(out)returns the sameRc.Out of scope
flatten.MaintainPopulation.populationstill holds its input as an inlineRc<OperatorNode>, so the flat form embeds that subgraph rather than linking it by id.pre_asap::canonicalize,pre_asap::cse,post_asap::cseanddag_export.Stack and validation
Revised logical foundation 3/6 · Previous: #560 · Next: #539 · Tracker: #528
Order: #567 → #560 → #537 → #539 → #540 → #541 → #542 → #543.
flatten: its payload isOperator<NodeId>, the runtime's 374-line conversion from the wire types became amap_childrencall, andEdgeRole/GroupingEdgeCompatibilitymoved there. feat(runtime): export timed plans as PhysicalASAPDAG and compile them #541–refactor(ir): remove the legacy QueryExpr and SummaryExpr IRs; move docs and DAG viewer to the unified IR #543 each carry one port commit.🤖 Generated with Claude Code