Stability: Internal — see Stability Tiers.
This document describes the design, data-flow, and implementation of constant folding in Titan. It assumes familiarity with README.md and docs/runtime.md.
Implementation status: Implemented as a dedicated pre-optimization pass (Pass 0) in the optimizer’s multi-pass framework. Both scalar and relational constant folding are live. See “Pass 0: Constant Folding” in docs/optimizer.md.
The goal is to make any scalar or relational sub-tree that is functionally constant collapse to a LiteralNode or a TableLiteralNode, using one evaluation engine — the existing runtime.
| Term | Meaning |
|---|---|
| pure | The node never mutates database or VM state. |
| deterministic | Given identical inputs the node always returns the same output. |
| functional | pure and deterministic. A functional node is safe to fold. |
functional is not a stored property; it is derived as deterministic && readonly. Use PlanNode.isFunctional(physical):
// plan-node.ts
public static isFunctional(physical: PhysicalProperties): boolean {
return (physical.deterministic !== false) && (physical.readonly !== false);
}A helper isFunctional(node) returns the effective value.
During a single post-order DFS every PlanNode is assigned a ConstInfo:
/* not exported – internal to folding pass */
interface ConstInfoConst { kind: 'const'; node: PlanNode; }
interface ConstInfoDep { kind: 'dep'; deps: Set<AttributeId>; }
interface ConstInfoVar { kind: 'non-const'; }
type ConstInfo = ConstInfoConst | ConstInfoDep | ConstInfoVar;Rules (scalar nodes):
LiteralNode→constwith its value.ColumnReference(attrId)→depwith{attrId}.- Any other scalar node
- If not functional →
non-const. - Else inspect children:
• If all children
const→ evaluate immediately (see §4) →const. • If all children ∈ {const,dep} →depwith union of childdeps. • Otherwise →non-const.
- If not functional →
Relational nodes are initially non-const; attributes produced will be analysed in the top-down pass.
The Map<PlanNodeId, ConstInfo> is stored on the pass context.
We now walk the relational tree from root to leaves carrying a set
knownConstAttrs: Set<AttributeId>.
For a relational node R with output attributes A₀…Aₙ:
- For each projection / column-producing expression
Eᵢ:- Look up
ConstInfoofEᵢ. - If
kind === 'const'→ markAᵢconstant. - If
kind === 'dep'anddeps ⊆ knownConstAttrs→ we can now foldEᵢ(evaluate & replace) and markAᵢconstant.
- Look up
- After processing, add all newly constant
AᵢtoknownConstAttrsand recurse to child relations, translating attribute IDs through projection / join mapping.
The pass converges in a single traversal because the set of constant attributes only grows and every node is visited once.
When we decide to fold a scalar expression expr:
const instr = emitPlanNode(expr, new EmissionContext(db /* temp */));
const sched = new Scheduler(instr);
const rtCtx: RuntimeContext = {
db, stmt: null, params: {},
context: new RowContextMap(), tableContexts: new Map(),
enableMetrics: false
};
const out = sched.run(rtCtx);
const val = out instanceof Promise ? await out : out;
const lit = new LiteralNode(expr.scope, {type: 'literal', value: val});Notes
- There is no special row context. Column references resolve because they're replaced only when their source attribute is already folded to a literal, thus no
ColumnReferenceNodesurvives evaluation. - The scheduler may or may not be async; both paths are handled.
- Any exception aborts folding and leaves the original node untouched.
Relational subtrees classified as const are replaced with TableLiteralNode via deferred materialization:
- The relational subtree is emitted into an instruction tree and a
Scheduleris created. - A
MaterializingAsyncIterablewraps execution: on first iteration it runs the scheduler, collects all rows, caches them, and yields. Subsequent iterations yield from the cache. - A
TableLiteralNodeis constructed with the iterable, preserving the original node'sRelationTypeand attribute IDs (viapredefinedAttributes).
This keeps the optimizer synchronous while deferring actual execution to first runtime access. Attribute ID preservation ensures parent ColumnReference nodes continue to resolve correctly.
What gets folded:
VALUESclauses with all-literal cells- Constant subqueries (
SELECT 1+2, 'hello') — Project over SingleRow with const expressions - Deterministic TVF calls with constant arguments (e.g.,
query_plan('SELECT 1')) - Any functional relational node whose entire child subtree is const
What does NOT get folded:
- Anything referencing actual tables (
Retrievenodes are non-const) - Non-deterministic expressions (
random(),datetime('now')) - Mutating operations (marked
readonly: false) - Void-type nodes (e.g.,
Block) — border detection recurses through these to find inner foldable nodes
- Implemented as a dedicated pre-optimization pass (Pass 0) in the pass framework. The pass:
- Runs bottom-up classification.
- Runs top-down propagation.
- Replaces foldable scalar subtrees with
LiteralNodeand relational subtrees withTableLiteralNode.
Builders do not perform folding themselves; they rely on the optimizer pass.
- Functional is derived:
isFunctional(physical) = deterministic && readonly. - Side-effecting or non-deterministic scalar operators must set
readonly=falseordeterministic=falsevia their physical property computation so they are never folded.
| Hazard | Mitigation |
|---|---|
Side-effects (random(), now(), UDF with mutations) |
Those nodes have functional=false; never folded. |
| Future async UDFs | Scheduler returns Promise; folding awaits it. |
| Column references before producer folded | Two-phase (bottom-up + top-down) ensures dependency sets resolved first. |
- Cost-based cut-off: skip folding very large expression trees if projected gain is low.
Relational constant detection and replacement— Done. Foldable relational subtrees replaced withTableLiteralNodevia deferred materialization.- Cost-based cut-off heuristics for very large expression trees.
- Optional PRAGMA to enable/disable constant folding for debugging.
- Broader test coverage and golden plans for complex dependency scenarios.
- functional = deterministic && readonly indicates fold-safety.
- Bottom-up builds dependency sets, top-down resolves them.
- Scalar constants →
LiteralNode; relational constants →TableLiteralNode(deferred materialization). - Evaluation uses the existing runtime through a mini-Scheduler.
- No environment-variable logic is needed in the folding path.