Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion nodedb-sql/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ fn plan_statements(
let mut dml_plans = if is_upsert {
planner::dml::plan_upsert(ins, catalog)?
} else {
planner::dml::plan_insert(ins, catalog)?
planner::dml::plan_insert(ins, catalog, &functions, temporal)?
};
plans.append(&mut dml_plans);
}
Expand Down
6 changes: 5 additions & 1 deletion nodedb-sql/src/planner/cte/join_link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
use sqlparser::ast::{self, SetExpr};

use crate::error::{Result, SqlError};
use crate::functions::registry::FunctionRegistry;
use crate::parser::normalize::{normalize_ident, table_name_from_factor};
use crate::planner::select::CteCatalog;
use crate::resolver::columns::TableScope;
use crate::temporal::TemporalScope;
use crate::types::*;

/// Extract recursive info from the AST when normal planning fails
Expand All @@ -24,6 +26,8 @@ pub(super) fn extract_recursive_info(
expr: &SetExpr,
cte_name: &str,
catalog: &dyn SqlCatalog,
functions: &FunctionRegistry,
temporal: TemporalScope,
) -> Result<RecursiveInfo> {
let select = match expr {
SetExpr::Select(s) => s,
Expand Down Expand Up @@ -69,7 +73,7 @@ pub(super) fn extract_recursive_info(
// The working table is absent from the ordinary catalog, so the CTE name
// resolves as an open relation for the duration of this arm.
let arm_catalog = CteCatalog::open(catalog, cte_name);
let scope = TableScope::resolve_from(&arm_catalog, &select.from)?;
let scope = TableScope::resolve_from(&arm_catalog, functions, temporal, &select.from)?;

// Extract the join link from the ON condition.
let join_link = if let (Some(real_alias), Some(cte_al), Some(on_expr)) =
Expand Down
6 changes: 5 additions & 1 deletion nodedb-sql/src/planner/cte/recursive_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ pub fn plan_recursive_cte(
distinct,
},
catalog,
functions,
temporal,
)
}

Expand Down Expand Up @@ -172,6 +174,8 @@ fn plan_recursive_scan_from_parts(
base: &SqlPlan,
parts: &RecursiveParts<'_>,
catalog: &dyn SqlCatalog,
functions: &FunctionRegistry,
temporal: crate::TemporalScope,
) -> Result<SqlPlan> {
let RecursiveParts {
left,
Expand All @@ -198,7 +202,7 @@ fn plan_recursive_scan_from_parts(
// shape directly instead of attempting ordinary planning and swallowing
// whichever error happens to occur first.
let (recursive_filters, join_link) =
super::join_link::extract_recursive_info(right, cte_name, catalog)?;
super::join_link::extract_recursive_info(right, cte_name, catalog, functions, temporal)?;

// The anchor plan carries the CTE's resolved output columns; propagate
// them so the recursive scan self-describes its output schema.
Expand Down
24 changes: 15 additions & 9 deletions nodedb-sql/src/planner/dml/insert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ use crate::error::Result;
use crate::types::*;

/// Plan an INSERT statement.
pub fn plan_insert(ins: &ast::Insert, catalog: &dyn SqlCatalog) -> Result<Vec<SqlPlan>> {
pub fn plan_insert(
ins: &ast::Insert,
catalog: &dyn SqlCatalog,
functions: &crate::functions::registry::FunctionRegistry,
temporal: crate::TemporalScope,
) -> Result<Vec<SqlPlan>> {
let (table_name, info) = resolve_target(ins, "INSERT", catalog)?;
let target_scope = target_scope(&table_name, &info)?;

Expand All @@ -39,13 +44,9 @@ pub fn plan_insert(ins: &ast::Insert, catalog: &dyn SqlCatalog) -> Result<Vec<Sq
if let Some(source) = &ins.source
&& let ast::SetExpr::Select(select) = &*source.body
{
let column_map = bind_insert_select_columns(catalog, &columns, select, &info)?;
let source_plan = super::super::select::plan_query(
source,
catalog,
&crate::functions::registry::FunctionRegistry::new(),
crate::TemporalScope::default(),
)?;
let column_map =
bind_insert_select_columns(catalog, functions, temporal, &columns, select, &info)?;
let source_plan = super::super::select::plan_query(source, catalog, functions, temporal)?;
return Ok(vec![SqlPlan::InsertSelect {
target: table_name,
source: Box::new(source_plan),
Expand Down Expand Up @@ -173,7 +174,12 @@ mod tests {
let sqlparser::ast::Statement::Insert(ins) = &statements[0] else {
panic!("expected an INSERT statement");
};
let mut plans = plan_insert(ins, catalog)?;
let mut plans = plan_insert(
ins,
catalog,
&crate::functions::registry::FunctionRegistry::new(),
crate::TemporalScope::default(),
)?;
Ok(plans.remove(0))
}

Expand Down
4 changes: 3 additions & 1 deletion nodedb-sql/src/planner/dml_helpers/insert_select_bind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ use crate::types::*;
/// copies the row unchanged and needs no per-column expression.
pub(crate) fn bind_insert_select_columns(
catalog: &dyn SqlCatalog,
functions: &crate::functions::registry::FunctionRegistry,
temporal: crate::TemporalScope,
target_columns: &[String],
select: &ast::Select,
target: &CollectionInfo,
Expand Down Expand Up @@ -55,7 +57,7 @@ pub(crate) fn bind_insert_select_columns(

// The source scope gates every column the SELECT list names: one the
// source does not carry raises `UnknownColumn` here, at plan time.
let source_scope = TableScope::resolve_from(catalog, &select.from)?;
let source_scope = TableScope::resolve_from(catalog, functions, temporal, &select.from)?;
let scope = ColumnScope::Relations(&source_scope);

let mut bound = Vec::with_capacity(names.len());
Expand Down
1 change: 1 addition & 0 deletions nodedb-sql/src/planner/join/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub fn plan_join_from_select(
outer_projection: projection,
outer_scope: scope,
catalog,
functions,
temporal,
})?));
}
Expand Down
13 changes: 11 additions & 2 deletions nodedb-sql/src/planner/lateral/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use super::subquery::{
reject_lateral_offset,
};
use crate::error::{Result, SqlError};
use crate::functions::registry::FunctionRegistry;
use crate::resolver::ColumnScope;
use crate::resolver::columns::{ResolvedTable, TableScope};
use crate::resolver::expr::convert_expr;
Expand Down Expand Up @@ -39,6 +40,7 @@ pub struct LateralJoinArgs<'a> {
/// so a correlated reference to an outer relation resolves.
pub outer_scope: &'a TableScope,
pub catalog: &'a dyn SqlCatalog,
pub functions: &'a FunctionRegistry,
pub temporal: TemporalScope,
}

Expand All @@ -56,6 +58,7 @@ pub fn plan_lateral_join(args: LateralJoinArgs<'_>) -> Result<SqlPlan> {
outer_projection,
outer_scope,
catalog,
functions,
temporal,
} = args;
let select = match subquery.body.as_ref() {
Expand Down Expand Up @@ -103,6 +106,8 @@ pub fn plan_lateral_join(args: LateralJoinArgs<'_>) -> Result<SqlPlan> {
outer_projection,
outer_scope,
catalog,
functions,
temporal,
})
} else if has_equi && analysis.non_equi.is_empty() {
// Equi-correlated, no LIMIT: rewrite as a regular hash join.
Expand Down Expand Up @@ -255,6 +260,8 @@ struct LateralTopKPlanArgs<'a> {
outer_projection: Vec<Projection>,
outer_scope: &'a TableScope,
catalog: &'a dyn SqlCatalog,
functions: &'a FunctionRegistry,
temporal: TemporalScope,
}

/// Plan the `LateralTopK` variant: equi-correlated + ORDER BY + LIMIT k.
Expand All @@ -271,6 +278,8 @@ fn plan_lateral_top_k(args: LateralTopKPlanArgs<'_>) -> Result<SqlPlan> {
outer_projection,
outer_scope,
catalog,
functions,
temporal,
} = args;
// Build a bare inner Scan without correlation filters (those are injected
// at runtime per outer row).
Expand All @@ -283,8 +292,8 @@ fn plan_lateral_top_k(args: LateralTopKPlanArgs<'_>) -> Result<SqlPlan> {
// The Top-K plan does not retain the inner alias, but it must still reject
// malformed aliases before expressions referencing them are lowered.
let _inner_alias = extract_inner_alias(select)?;
let inner_scope =
TableScope::resolve_from(catalog, &select.from)?.nested_in(outer_scope.clone());
let inner_scope = TableScope::resolve_from(catalog, functions, temporal, &select.from)?
.nested_in(outer_scope.clone());
let inner_filters =
inner_non_correlated_filters(select, outer_alias.as_deref().unwrap_or(""), &inner_scope)?;

Expand Down
2 changes: 2 additions & 0 deletions nodedb-sql/src/planner/select/comma_lateral.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub(super) fn try_plan_comma_lateral(
select: &Select,
scope: &TableScope,
catalog: &dyn SqlCatalog,
functions: &crate::functions::registry::FunctionRegistry,
temporal: TemporalScope,
) -> Result<Option<SqlPlan>> {
if select.from.len() != 2 || !is_lateral_derived(&select.from[1].relation) {
Expand Down Expand Up @@ -74,6 +75,7 @@ pub(super) fn try_plan_comma_lateral(
outer_projection: projection,
outer_scope: scope,
catalog,
functions,
temporal,
})
.map(Some)
Expand Down
10 changes: 8 additions & 2 deletions nodedb-sql/src/planner/select/derived_from.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,14 @@ pub(in crate::planner::select) fn try_plan_derived_from(
// Replan the outer SELECT against a catalog that resolves the alias to
// the columns the subquery projects. The outer can reference `alias.col`
// qualified or unqualified.
let relation =
crate::resolver::derived::infer_subquery_relation(catalog, &alias_name, subquery)?;
let relation = crate::resolver::derived::infer_subquery_relation(
catalog,
&alias_name,
subquery,
Some(&inner_plan),
functions,
temporal,
)?;
let derived_catalog = CteCatalog {
inner: catalog,
relations: vec![(
Expand Down
9 changes: 8 additions & 1 deletion nodedb-sql/src/planner/select/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,14 @@ pub fn plan_query(
.map(|column| check_ast_identifier(&column.name))
.collect::<Result<_>>()?;
let cte_plan = plan_query(&cte.query, catalog, functions, temporal)?;
let info = infer_subquery_relation(catalog, &name, &cte.query)?;
let info = infer_subquery_relation(
catalog,
&name,
&cte.query,
Some(&cte_plan),
functions,
temporal,
)?;
definitions.push((name.clone(), cte_plan));
relations.push((name, rename_output_columns(info, &declared)));
}
Expand Down
6 changes: 3 additions & 3 deletions nodedb-sql/src/planner/select/select_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ pub(super) fn plan_select(
// attrs, so ORDER BY and the tail clauses resolve its columns.
return Ok(PlannedSelect {
plan,
scope: TableScope::resolve_from(catalog, &select.from)?,
scope: TableScope::resolve_from(catalog, functions, temporal, &select.from)?,
});
}

Expand All @@ -61,7 +61,7 @@ pub(super) fn plan_select(
}

// 1. Resolve FROM tables.
let scope = TableScope::resolve_from(catalog, &select.from)?;
let scope = TableScope::resolve_from(catalog, functions, temporal, &select.from)?;

// 2. Handle constant queries (no FROM clause): SELECT 1, SELECT 'hello', etc.
if select.from.is_empty() {
Expand Down Expand Up @@ -111,7 +111,7 @@ pub(super) fn plan_select(
}

// 3b. Comma-LATERAL syntax: `FROM t, LATERAL (SELECT ...) x`.
if let Some(plan) = try_plan_comma_lateral(select, &scope, catalog, temporal)? {
if let Some(plan) = try_plan_comma_lateral(select, &scope, catalog, functions, temporal)? {
return Ok(PlannedSelect { plan, scope });
}

Expand Down
2 changes: 1 addition & 1 deletion nodedb-sql/src/planner/subquery/exists.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub(super) fn plan_exists_subquery(
});
};

let local = TableScope::resolve_from(catalog, &select.from)?;
let local = TableScope::resolve_from(catalog, functions, temporal, &select.from)?;
let nested = local.clone().nested_in(outer.clone());

// EXISTS discards the projected values. The conversion still runs so a
Expand Down
40 changes: 33 additions & 7 deletions nodedb-sql/src/resolver/columns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use std::collections::HashMap;
use nodedb_types::DatabaseId;

use crate::error::{Result, SqlError};
use crate::functions::registry::FunctionRegistry;
use crate::parser::normalize::table_name_from_factor;
use crate::temporal::TemporalScope;
use crate::types::{CollectionInfo, ColumnInfo, SqlCatalog};

/// Synthetic temporal columns an audit read injects into every version row.
Expand Down Expand Up @@ -243,15 +245,21 @@ impl TableScope {
}

/// Resolve tables from a FROM clause.
///
/// A derived-subquery factor is planned here so its relation carries the
/// search cells its plan produces; the planner context (`functions`,
/// `temporal`) is required for that and for no other arm.
pub fn resolve_from(
catalog: &dyn SqlCatalog,
functions: &FunctionRegistry,
temporal: TemporalScope,
from: &[sqlparser::ast::TableWithJoins],
) -> Result<Self> {
let mut scope = Self::new();
for table_with_joins in from {
scope.resolve_table_factor(catalog, &table_with_joins.relation)?;
scope.resolve_table_factor(catalog, functions, temporal, &table_with_joins.relation)?;
for join in &table_with_joins.joins {
scope.resolve_table_factor(catalog, &join.relation)?;
scope.resolve_table_factor(catalog, functions, temporal, &join.relation)?;
}
}
Ok(scope)
Expand All @@ -260,6 +268,8 @@ impl TableScope {
fn resolve_table_factor(
&mut self,
catalog: &dyn SqlCatalog,
functions: &FunctionRegistry,
temporal: TemporalScope,
factor: &sqlparser::ast::TableFactor,
) -> Result<()> {
// ARRAY_*(...) table-valued function: synthesize a ResolvedTable
Expand All @@ -270,10 +280,13 @@ impl TableScope {
return Ok(());
}
// Derived subquery, LATERAL or not: register the alias as the relation
// its projection list exposes, so a column reference on the alias
// resolves without a catalog lookup. The inner plan is built
// separately.
// its projection list exposes — a column reference on the alias
// resolves without a catalog lookup. A non-LATERAL subquery is planned
// so its relation carries the search cells the plan produces; a
// correlated LATERAL one cannot be planned at scope time, and the
// lateral planner routes its shapes without those cells.
if let sqlparser::ast::TableFactor::Derived {
lateral,
subquery,
alias: Some(alias),
..
Expand All @@ -285,8 +298,21 @@ impl TableScope {
.iter()
.map(|column| crate::reserved::check_ast_identifier(&column.name))
.collect::<Result<_>>()?;
let info =
crate::resolver::derived::infer_subquery_relation(catalog, &alias_str, subquery)?;
let plan = if *lateral {
None
} else {
Some(crate::planner::select::plan_query(
subquery, catalog, functions, temporal,
)?)
};
let info = crate::resolver::derived::infer_subquery_relation(
catalog,
&alias_str,
subquery,
plan.as_ref(),
functions,
temporal,
)?;
self.add(ResolvedTable {
name: alias_str.clone(),
alias: Some(alias_str),
Expand Down
Loading
Loading