graphforge_rel/lib.rs
1//! GraphForge Graph IR → DataFusion relational lowering.
2//!
3//! # Milestone status
4//!
5//! - M12 #574 — IR expression lowering to DataFusion `Expr`
6//! - M12 #573 — Wire SessionContext
7//! - M12 #575 — Lower simple GraphOps (Filter, Project, …)
8//! - M12 #576 — Lower NodeScan and Expand
9//! - M12 #578 — Graph-native Extension stubs + `explain_logical` ← **this issue**
10#![forbid(unsafe_code)]
11
12pub mod expr;
13pub use expr::{ExprLowerer, LoweringError, VarMap, ir_literal_to_scalar, scalar_to_ir_literal};
14
15pub mod lowerer;
16pub use lowerer::GraphPlanLowerer;
17
18pub mod calendar;
19
20pub mod temporal;
21
22pub use graphforge_core::GfError;
23pub use graphforge_ir::GraphPlan;
24use graphforge_ontology::OntologyHandle;
25
26/// A DataFusion [`datafusion::logical_expr::LogicalPlan`] produced by the
27/// relational lowering pass.
28///
29/// Using a type alias means all downstream code (including `graphforge-exec`) works
30/// directly with the native DataFusion type without any wrapping overhead.
31pub type LogicalPlan = datafusion::logical_expr::LogicalPlan;
32
33/// Lower a [`GraphPlan`] to a DataFusion [`LogicalPlan`].
34///
35/// Delegates to [`GraphPlanLowerer`] with `catalog = None` and
36/// `ontology = None` (exploratory mode). Scan operators (`NodeScan`,
37/// `Expand`) return [`GfError::Plan`] until #576 is complete.
38///
39/// # Errors
40///
41/// Returns [`GfError::Plan`] if any operator in the pipeline cannot be
42/// lowered (e.g. graph-native scan operators not yet implemented in #576).
43pub fn lower(plan: &GraphPlan) -> Result<LogicalPlan, GfError> {
44 GraphPlanLowerer::new(None, None).lower_plan(plan)
45}
46
47/// Lower a [`GraphPlan`] and run DataFusion's analyzer + optimizer over it.
48///
49/// Uses a transient [`SessionContext`](datafusion::prelude::SessionContext)
50/// with no registered catalog — the lowered plan is self-contained (scans use
51/// [`LogicalTableSource`](datafusion::logical_expr::logical_plan::LogicalTableSource)
52/// with static schemas), so optimisation needs no external state. Lowering
53/// runs in exploratory mode (`catalog = None`, `ontology = None`).
54///
55/// # Errors
56///
57/// Returns [`GfError::Plan`] if lowering fails or the analyzer/optimizer
58/// rejects the plan (e.g. unresolved `$param` placeholders).
59pub fn lower_and_optimize(plan: &GraphPlan) -> Result<LogicalPlan, GfError> {
60 let lowered = lower(plan)?;
61 let ctx = datafusion::prelude::SessionContext::new();
62 ctx.state()
63 .optimize(&lowered)
64 .map_err(|e| GfError::Plan(e.to_string()))
65}
66
67/// Render a [`GraphPlan`]'s optimised DataFusion [`LogicalPlan`] as indented
68/// text (with per-node schemas) for the `explain` LogicalPlan stage.
69///
70/// Lowers in exploratory mode (no ontology). Falls back to the
71/// pre-optimisation plan when the analyzer/optimizer rejects it — graph-native
72/// [`Extension`](datafusion::logical_expr::Extension) stub nodes carry no
73/// optimiser semantics yet, and `$param` placeholders cannot be type-coerced
74/// until execution time. In both cases the un-optimised plan is still valid,
75/// inspectable output.
76///
77/// # Errors
78///
79/// Returns [`GfError`] if the plan cannot be lowered at all.
80pub fn explain_logical(plan: &GraphPlan) -> Result<String, GfError> {
81 explain_logical_with(plan, None)
82}
83
84/// Like [`explain_logical`] but lowers with an optional [`OntologyHandle`] so
85/// that typed edge scans and variable-length expands can resolve relation-type
86/// names. Used by the logical-plan golden suite (which binds against a formal
87/// ontology for deterministic type IDs).
88///
89/// # Errors
90///
91/// Returns [`GfError`] if the plan cannot be lowered.
92pub fn explain_logical_with(
93 plan: &GraphPlan,
94 ontology: Option<&OntologyHandle>,
95) -> Result<String, GfError> {
96 explain_logical_with_catalog(plan, None, ontology)
97}
98
99/// Like [`explain_logical_with`] but also threads a
100/// [`GraphCatalog`](graphforge_storage::GraphCatalog) so property accesses (e.g.
101/// `n.name`) resolve to their column names via the catalog's `PropId → name`
102/// map instead of falling back to `prop_<id>` placeholders. Used by the engine
103/// facade's `explain`, which lowers against the instance's runtime catalog.
104///
105/// # Errors
106///
107/// Returns [`GfError`] if the plan cannot be lowered.
108pub fn explain_logical_with_catalog(
109 plan: &GraphPlan,
110 catalog: Option<&graphforge_storage::GraphCatalog>,
111 ontology: Option<&OntologyHandle>,
112) -> Result<String, GfError> {
113 let lowered = GraphPlanLowerer::new(catalog, ontology).lower_plan(plan)?;
114 let ctx = datafusion::prelude::SessionContext::new();
115 let final_plan = ctx.state().optimize(&lowered).unwrap_or(lowered);
116 Ok(final_plan.display_indent_schema().to_string())
117}