Stability: Internal — see Stability Tiers.
How operations slide across the virtual-table boundary: the RetrieveNode
abstraction, the module capability API, access-path selection, and the correlated
(lateral) access model. Table-valued functions advertise the same physical properties
a virtual table does, so their declaration surface lives here too.
The Quereus optimizer features a comprehensive push-down infrastructure built around the RetrieveNode abstraction. This system enables virtual table modules to execute arbitrary query pipelines within their own execution context, providing a clean boundary between Quereus execution and module-specific optimization.
Core Concept: Every TableReferenceNode is wrapped in a RetrieveNode at build time, marking the exact boundary where data transitions from virtual table module execution to Quereus execution.
// Builder automatically wraps table references
export function buildTableReference(fromClause: AST.FromClause, context: PlanningContext): RetrieveNode {
const tableRef = new TableReferenceNode(/* ... */);
return new RetrieveNode(context.scope, tableRef, tableRef); // pipeline starts as just the table
}Structure:
RetrieveNode
└─ pipeline: RelationalPlanNode (operations handled by the module)
└─ TableReferenceNode (leaf table reference)
[bindings: ScalarPlanNode[]] (captured params/correlated expressions)
-
Pushdown rule: When sliding a
Filterdown into aRetrieve, the optimizer:- Normalizes the predicate, extracts constraints for the
Retrievetable, and constructs a supported-only predicate fragment. - Inserts only that fragment as a
Filterinside theRetrievepipeline. - Leaves any residual (unsupported) predicate above the
Retrieveboundary. - Merges newly referenced bindings (parameters/correlations) into
Retrieve.bindings. - Declines outright once the access path is committed: if the target
Retrievealready carries an index-stylemoduleCtx, no predicate is pushed into it at all. From that pointrule-select-access-pathbuilds the physical leaf fromaccessPlan+moduleCtx.residualPredicateand never readsRetrieve.source, so a fragment placed there would execute nowhere and the query would return unfiltered rows. TheFilterstays above the boundary instead, where grow-retrieve re-probesgetBestAccessPlan()with the constraint on the next fixed-point iteration and residualizes whatever the module declines — so nothing is lost by declining.Retrieve.sourceremains populated for the readers that still walk it (binding collection, and the constraint sweep intrySortAbsorbViaIndexOrdering); it is dead only as an execution channel.
- Normalizes the predicate, extracts constraints for the
-
Grow-retrieve rule: When sliding
Retrieveupward over aFilter(index-style fallback):- The rule mirrors the pushdown behavior: only supported fragments of the enveloped node are placed beneath
Retrieveas a newFilter. The residual remains above. - Bindings are collected from the added fragment and merged into
Retrieve.bindings. - Subquery-bearing residual carve-out: the index-style residual is normally stashed on
moduleCtx.residualPredicateand rebuilt into aFilterbyrule-select-access-path. But a residual that contains ANY subquery (IN (SELECT …),NOT IN (SELECT …),EXISTS, a scalar subquery, or any future subquery form) is instead kept above the grownRetrieveas a realFilterNodeat grow time. A stashed subquery would carry its own innerRetrieveoutside the plan tree the bottom-up physical pass walks, leaving it unphysicalized (it surfaces as the missing-emitter error in the physicalization invariant below). Detection is structural — the residual is kept above iff any node in it is relational (a subquery is the only way aRelationalPlanNodehangs beneath a scalar predicate), so new subquery node types are covered automatically. Correlation is irrelevant — a self-contained subquery buries an innerRetrievejust the same as a correlated one.
- The rule mirrors the pushdown behavior: only supported fragments of the enveloped node are placed beneath
This policy ensures the Retrieve pipeline is always a precise description of what the module/index can handle; unsupported parts never enter the boundary.
Invariant: OPT-025
extractConstraintsForTable(plan, key) answers "which predicates in this subtree constrain
table instance key?". Both callers that place work on an access path use it:
rule-select-access-path sweeps Retrieve.source, and trySortAbsorbViaIndexOrdering
(grow-retrieve) sweeps the whole subtree beneath an absorbed Sort — the only caller that
looks past a single Filter's own predicate, which is why a bug here needs an ORDER BY the
table's own key walk satisfies.
The subtree is not the same thing as the scope. A subquery body is a RelationalPlanNode
hanging beneath a scalar expression, so where exists (select 1 from t where t.s = a.i)
puts the inner t.s = a.i inside the outer Filter's subtree. The sweep (walkPredicatesConstraining)
therefore descends into everything but only visits a predicate when the target table
reference is reachable through relational-child links from the predicate's own node — i.e.
when the table is genuinely in that predicate's input. An inner scan of the same table still
collects its own predicates, because the recursion still enters subquery bodies; what it finds
there simply cannot mark an enclosing node's input.
Note the distinction from correlation. A constraint whose value side references an outer
attribute (PredicateConstraint.correlated) is a supported feature — it is how a correlated
seek is pushed into an inner scan. What is never legal is attributing an inner scope's
predicate to an outer table: the unhandled-constraint path turns it into
moduleCtx.residualPredicate, and rule-select-access-path then materializes a Filter
reading a column the outer relation does not have (runtime "No row context found for column
…"), or — under NOT EXISTS, where the hoisted conjunct happens to be satisfiable — silently
drops rows.
The gate is also the only protection against one shape the correlated flag cannot see: an
outer column compared to a constant inside the subquery body
(where not exists (select 1 from t where a.i = 2)). Its value side is a literal, so
computeCoveredKeysForConstraints would treat it as an ordinary covering equality on a —
letting extractCoveredKeysForTable claim ≤1 row for a relation with many, and letting
rule-select-access-path seek on it, which drops every row under NOT EXISTS. A subquery
over the same table is not this shape: the inner scan is a distinct
TableReferenceNode with its own attribute ids, so exists (select 1 from a a2 where a2.id = 2) never matched the outer instance's attributes in the first place.
SetOperation(UNION,INTERSECT,EXCEPT,DIFF) is excluded from the grow-retrieve structural pass. Sliding aRetrieveboundary across set operations can cause structural oscillation and provides little benefit to index-style modules. Predicate push-down into the branches remains supported via the supported-only policy.
Invariant: OPT-020
- During the physical selection pass, all
Retrievenodes must be rewritten to concrete access nodes (SeqScan,IndexScan, orIndexSeek) orRemoteQuery.validatePhysicalNodeTypeasserts this, but it runs only undertuning.debug.validatePlan, which is off by default — so in a release build a survivingRetrievesurfaces as a missing-emitter error rather than as that assertion.
- For index-style modules, full primary-key equality (including parameterized values) will select
IndexSeekeven if the provider’shandledFiltersordering differs from planner constraint extraction. The optimizer aligns constraints by column index and constructs dynamic seek keys from parameters/correlated expressions.
query_plan(sql)exposesRETRIEVErows with logical properties includingbindingsCountandbindingsNodeTypes, which reveal whether parameters and/or correlated column references have been captured by the pipeline.- For test assertions, prefer checking for the presence of
ParameterReferencenodes in the plan (logical indicator of binding presence) rather than relying onRETRIEVEpresence post-physical selection, since physical rules may replaceRetrievewith concrete access operators.
VirtualTableModule Interface:
interface VirtualTableModule {
// Query-based push-down
supports?(node: PlanNode): SupportAssessment | undefined;
// Index-based access
getBestAccessPlan?(req: BestAccessPlanRequest): BestAccessPlanResult;
}
interface SupportAssessment {
cost: number; // Module's cost estimate for executing this pipeline
ctx?: unknown; // Opaque context data cached for runtime execution
}VirtualTable Interface:
interface VirtualTable {
// Runtime execution of pushed-down pipelines
executePlan?(db: Database, plan: PlanNode, ctx?: unknown): AsyncIterable<Row>;
// Standard index-based query execution
query?(filterInfo: FilterInfo): AsyncIterable<Row>;
}1. Query-based Push-down (implements supports() + executePlan())
- Module analyzes entire query pipelines
- Returns cost assessment for execution within module
- Examples: SQL federation modules, document databases, remote APIs
2. Index-based Access (implements getBestAccessPlan() + query())
- Module exposes index capabilities
- Quereus pushes individual predicates via BestAccessPlan API
- Examples: MemoryTable, SQLite vtabs, file-based storage
3. Hybrid Modules (can implement both, but they're mutually exclusive per query)
- Modules can provide both interfaces
- Optimizer chooses based on cost assessment
The ruleSelectAccessPath optimizer rule handles the routing decision:
export function ruleSelectAccessPath(node: PlanNode, context: OptContext): PlanNode | null {
if (!(node instanceof RetrieveNode)) return null;
const vtabModule = node.vtabModule;
// Query-based push-down takes priority
if (vtabModule.supports) {
const assessment = vtabModule.supports(node.source);
if (assessment) {
return new RemoteQueryNode(node.scope, node.source, node.tableRef, assessment.ctx);
}
// Module declined - fall back to sequential scan
return createSeqScan(node.tableRef);
}
// Index-based access
if (vtabModule.getBestAccessPlan) {
return createIndexBasedAccess(node, context);
}
// Default sequential scan
return createSeqScan(node.tableRef);
}RemoteQueryNode:
- Represents execution of a pipeline within a virtual table module
- Calls
VirtualTable.xExecutePlan()at runtime - Passes the original plan pipeline and cached context
Traditional Access Nodes:
SeqScanNode: Full table scanIndexScanNode: Index-based scan with filtersIndexSeekNode: Index-based point/range lookupsEmptyResultNode: Zero-row short-circuit at the access boundary (e.g.,IS NULLon NOT NULL column). Sibling nodeEmptyRelationNode(planner/nodes/empty-relation-node.ts) covers the schema-polymorphic empty case for general fold rules —EmptyResultNodestays bound to aTableReferenceNode(for EXPLAIN), whileEmptyRelationNodeis detached from any specific source. See Rule Families § Empty-relation folding.
- Modules that implement
getBestAccessPlancan returnindexNameandseekColumnIndexesto identify the chosen index and its key columns. When present,selectPhysicalNodeFromPlanbuilds seek keys from the correct constraint columns — not hardcoded to PK. - When these fields are absent, the legacy PK-based heuristic path (
selectPhysicalNodeLegacy) is used for backward compatibility. - Equality constraints that fully cover a primary or secondary index prefix are translated into
IndexSeekNodewith dynamic seek keys:- Seek keys are stored as scalar expressions (parameters or correlated refs), evaluated at runtime by the emitter and passed to the module via the existing
FilterInfo.argsmechanism. - Range bounds (>=/<=) similarly pass dynamic lower/upper expressions.
- Seek keys are stored as scalar expressions (parameters or correlated refs), evaluated at runtime by the emitter and passed to the module via the existing
This establishes a clean “call-like” boundary: Retrieve.bindings declares required inputs; physical access nodes evaluate those inputs and deliver them to the module.
A seek's FilterInfo is the only place a claimed predicate is enforced: once handledFilters[i] === true, rule-select-access-path folds filter i into seek keys and it exists as a Filter nowhere in the tree. FilterInfo.constraints records only the encoded form (column index, operator, argv slot) — not enough to rebuild the predicate, because a comparison's effective collation is resolved from the original expression's operand types (analysis/comparison-collation.ts), not from the column alone.
So IndexSeekNode also carries pushedConstraints: the exact PredicateConstraint objects the rule consumed, each with its sourceExpression. A later rule that wants to replace the seek's access method can therefore re-apply them (AND them with the exported combineResidualExpressions) or re-offer them to getBestAccessPlan, instead of having to decline because it cannot tell what predicate it would be dropping. rule-key-set-seek is the first consumer: it admits a seek as the key-set semi join's target and re-applies the combined predicate as a Filter directly above the new node (see optimizer-rules.md).
- Stamped at one site.
selectPhysicalNodestamps between the index-aware/legacy dispatch andreattachUnconsumedConstraints, descending through the collation-residualFiltera seek arm may wrap the leaf in — not at the individualnew IndexSeekNode(...)calls. - Contents. Exactly the constraints this seek turned into keys or bounds, in
constraintsorder. Deliberately not every claimed filter: a claimed-but-unconsumed reclaimable constraint is already re-applied above the leaf byreattachUnconsumedConstraints, and a claimed constraint outsideRECLAIMABLE_OPSis enforced nowhere today (see that constant's note), so including either would misdescribe the field. - Not always re-appliable in an arbitrary position.
rules/join/index-nested-loop.tsbuilds seeks from synthesized correlated equalities whosesourceExpressionreferences an outer-side attribute. Recorded faithfully, but a consumer must gate on where it intends to re-apply. orderingLoadBearingrides alongside on bothIndexScanNodeandIndexSeekNode: true whenrule-grow-retrievedropped aSortbecause this access plan advertised the matching ordering, so a rewrite that changes the leaf's emission order must decline.- Neither field appears in
getLogicalAttributes— they are provenance for rules, not EXPLAIN output.
Query-based Execution:
// emitRemoteQuery.ts
export function emitRemoteQuery(plan: RemoteQueryNode, ctx: EmissionContext): Instruction {
async function* run(rctx: RuntimeContext): AsyncIterable<Row> {
const table = plan.vtabModule.connect(/* ... */);
yield* table.executePlan!(rctx.db, plan.source, plan.moduleCtx);
}
return { params: [], run, note: `remoteQuery(${plan.tableRef.tableSchema.name})` };
}Index-based Execution:
- Uses existing
query()withFilterInfoparameter - Leverages
BestAccessPlanAPI for predicate push-down
Builder Integration:
- All table references automatically wrapped in
RetrieveNode - DML operations (INSERT/UPDATE/DELETE) extract
tableReffromRetrieveNode - Maintains backward compatibility with existing code
Optimizer Integration:
ruleSelectAccessPathregistered forPlanNodeType.Retrieve- Physical properties correctly propagated through
RemoteQueryNode - Cost estimation integrated with existing cost model
Runtime Integration:
RemoteQueryNodeemitter registered in runtime system- Error handling for modules without
xExecutePlan()implementation - Seamless execution alongside traditional access methods
ruleGrowRetrieve is a structural, capability-bounded sliding rule. It maximizes the
query segment each virtual table module executes for itself, without consulting cost.
Algorithm — registered on every relational node type in the Structural pass, which
runs top-down so a parent is visited before the RetrieveNode child it may slide into:
- Graft the parent operation onto the child
RetrieveNode's current pipeline, forming a candidate pipeline - Assess it with
module.supports(candidatePipeline), or the index-style fallback (getBestAccessPlan) - On support, replace the parent with a new
RetrieveNodecarrying the expanded pipeline - On decline, stop — the
RetrieveNodehas reached its maximum extent
// Example: Filter above table reference
Filter(condition)
└── RetrieveNode(source: TableRef)
// After ruleGrowRetrieve (assuming module supports filtering):
RetrieveNode(source: Filter(condition, TableRef))Key properties:
- Purely structural — no cost modeling during growth, so the segment boundary is deterministic and reproducible
- Module-bounded — a module evaluates exactly the operations it commits to handle
- Runs before physical selection, so access-path choice sees the final segment
- Establishes the "query segment" baseline every later push-down rule builds on
Modules can accept arbitrary nodes: supports() may accept complex subtrees, including joins across multiple tables that reside in the same module. When a module declares support for such a subtree, ruleGrowRetrieve will slide those operations into the RetrieveNode boundary, enabling efficient intra-module execution.
A correlated or lateral join is planned as an ordinary JoinNode and executed by the
nested-loop emitter: the right subtree is re-executed once per left row, with the
correlated values visible through the runtime context. When the right side reduces to a
seek on an indexed column, the
fan-out lookup join clusters those
per-row lookups into one concurrently-driven node.
There is no separate ApplyNode abstraction. Pushing correlation values into a module as
declared constraints — so the module, rather than the runtime, drives the seek — is
future work; see
docs/todo.md § Push-down & Federation Roadmap.
Table-valued functions can advertise relational and physical characteristics through an optional relationalAdvertisement field on TableValuedFunctionSchema. Without it, a TVF's logical returnType.keys / returnType.isSet are exposed but physical defaults are conservative (no key FDs, no ordering, no monotonicOn, default estimatedRows). With an advertisement, TableFunctionCallNode.computePhysical consumes it on the standard physical-property path so downstream rules (FD propagation, DISTINCT elimination, sort/monotonic-window rules, cardinality estimation) see the same information they get from a real vtab.
Advertisement surface — each field is either a static value or a TVFAdvertiseFn<T> that receives the call's operands and the schema and may return undefined to decline:
| Field | Type | Notes |
|---|---|---|
isSet |
boolean |
Overrides returnType.isSet when present. |
keys |
ReadonlyArray<ReadonlyArray<ColRef>> |
Output-column unique keys; lifted into physical.fds as key → other-cols FDs and into getType().keys. |
fds |
ReadonlyArray<FunctionalDependency> |
Additional (non-key) FDs over output columns. |
equivClasses |
ReadonlyArray<ReadonlyArray<number>> |
Equivalence classes; each class must have ≥ 2 members. |
ordering |
ReadonlyArray<{column, desc}> |
Output ordering. |
monotonicOnColumns |
ReadonlyArray<{column, direction, strict?}> |
Column-keyed monotonicity; preferred over monotonicOn because the node mints attribute IDs per call — the node translates column → attrId when assembling physical props. |
monotonicOn |
ReadonlyArray<MonotonicOnInfo> |
Direct form for advanced uses where the author already has the attrId. |
constantBindings |
ReadonlyArray<ConstantBinding> |
Columns pinned to a single value over the call. |
estimatedRows |
number |
Row-count estimate; the TableFunctionCallNode.estimatedRows getter consults this before falling back to the default. |
accessCapabilities |
PhysicalProperties['accessCapabilities'] |
ordinalSeek / asofRight. |
deterministic, readonly, idempotent |
boolean |
Overrides the FunctionFlags-derived defaults. |
Literal operand inspection — evaluateLiteralOperand(operand) (from schema/function.js) returns operand.expression.value when the operand is a literal and undefined otherwise. Use it in a TVFAdvertiseFn closure to declare parameter-dependent values:
estimatedRows: (operands) => {
const start = evaluateLiteralOperand(operands[0]);
const end = evaluateLiteralOperand(operands[1]);
if (typeof start === 'number' && typeof end === 'number' && end >= start) {
return end - start + 1;
}
return undefined; // Decline when bounds are non-literal.
},Validation — every advertised field is shape-checked against the call's column count and attribute set before it lands in physical. Bad advertisements (out-of-range column indices, empty FD dependents, equivalence classes of size < 1, duplicate ordering columns, etc.) are dropped silently with a single warning on the planner:tvf log channel — they never break planning. A TVFAdvertiseFn closure that throws is treated the same way. This guarantees a buggy third-party advertisement degrades to "no advertisement" instead of poisoning the optimizer.
Built-in annotations — the following TVFs ship with relational advertisements:
| TVF | Advertisement |
|---|---|
generate_series(start, end) |
isSet, keys=[[value]], ordering=[{value, asc}], monotonicOnColumns=[{value, asc, strict}], estimatedRows (when bounds are literal). |
json_each(json[, path]) |
isSet, keys=[[id]]. |
json_tree(json[, path]) |
isSet, keys=[[id]]. |
query_plan(sql) |
isSet, keys=[[id]]. |
table_info(table) |
isSet, keys=[[cid]]. |
index_info(table) |
isSet, keys=[[index_name, seq]]. |
foreign_key_info(table) |
isSet, keys=[[id, seq]]. |
unique_constraint_info(table) |
isSet, keys=[[id, seq]]. |
check_constraint_info(table) |
isSet, keys=[[id]]. |
assertion_info() |
isSet, keys=[[schema_name, name]]. |
function_info() |
isSet, keys=[[name, num_args]]. |
Non-deterministic or trace-only TVFs (execution_trace, row_trace, stack_trace, scheduler_program, schema_size, explain_assertion, schema) skip advertisement.
Relevant to materialized-view maintenance (deferred shape). The TVF relationalAdvertisement (keys / isSet) is the surface a lateral-TVF row-time materialized-view body would consume to bound a fan-out (base t cross join lateral json_each(t.arr) je): a base-row change maps to many backing rows that a prefix-delete + recomputed-fan-out maintenance would need to prove set on the backing PK. This shape is not in the current row-time eligibility gate — it is deferred to materialized-view-rowtime-general-bodies. combineJoinKeys (planner/util/key-utils.ts) now forms the product key (leftKey ∪ shiftedRightKey) for a keyed cross/lateral join (when both sides are keyed and neither is equi-covered), so keysOf surfaces the keyed cross-product key — see Joins § Keyed cross/inner (and lateral) product keys. The remaining lateral-TVF consumption work (proving a recomputed fan-out set on the backing PK) is tracked by materialized-view-rowtime-general-bodies. See Materialized Views.