Support qualified table names in single-table SQL queries
This commit is contained in:
parent
7c4cb70047
commit
be8e1388bc
@ -73,6 +73,7 @@ Quick examples:
|
||||
- Existential variables generate labeled nulls (`Term::Null`).
|
||||
- The current SQL support is intentionally narrow: `SELECT-FROM-WHERE-ORDER BY` over predicate-backed tables, equality predicates combined with `AND`, comma-join style multi-table queries, table aliases, and ordering by output-column names.
|
||||
- Stable SQL column names come from explicit catalog registration or the frontend `schema ...` command, including for empty tables; otherwise the default names are positional such as `c0` and `c1`.
|
||||
- Single-table SQL queries may use the table name as a qualifier when no alias is present.
|
||||
- Do not describe unsupported SQL features such as aggregates, grouping, or arbitrary expressions as implemented.
|
||||
- Relational and SQL modules should build on explicit schemas and logical plans, not call frontend helpers directly.
|
||||
- If you add parser, planner, or executor layers, keep their responsibilities separate.
|
||||
|
||||
@ -168,6 +168,7 @@ Current limits:
|
||||
|
||||
- default column names are positional such as `c0`, `c1`
|
||||
- stable names require explicit catalog registration or `schema ...` in the frontend
|
||||
- single-table queries may also use the table name as a qualifier when no alias is present
|
||||
- joins currently use comma-separated tables plus `WHERE` filtering
|
||||
- multi-table queries require qualified column names such as `Parent.child`
|
||||
- table aliases are supported via `FROM Parent AS p`
|
||||
|
||||
@ -73,7 +73,7 @@ pub fn plan_select(
|
||||
let (mut plan, input_schema) = plan_from_tables(&select.from, catalog)?;
|
||||
|
||||
if let Some(selection) = &select.selection {
|
||||
let predicate = plan_expr(selection, &input_schema)?;
|
||||
let predicate = plan_expr(selection, &input_schema, &select.from)?;
|
||||
plan = LogicalPlan::Filter {
|
||||
input: Box::new(plan),
|
||||
predicate,
|
||||
@ -82,7 +82,7 @@ pub fn plan_select(
|
||||
|
||||
if is_wildcard_projection(&select.projection) {
|
||||
let output_schema = plan.output_schema().clone();
|
||||
return maybe_apply_sort(plan, output_schema, &select.order_by);
|
||||
return maybe_apply_sort(plan, output_schema, &select.order_by, &select.from);
|
||||
}
|
||||
|
||||
let mut expressions = Vec::new();
|
||||
@ -90,11 +90,11 @@ pub fn plan_select(
|
||||
for (index, item) in select.projection.iter().enumerate() {
|
||||
match item {
|
||||
SelectItem::Expr { expr, alias } => {
|
||||
let planned_expr = plan_expr(expr, &input_schema)?;
|
||||
let planned_expr = plan_expr(expr, &input_schema, &select.from)?;
|
||||
let output_name = alias
|
||||
.clone()
|
||||
.unwrap_or_else(|| default_projection_name(expr, index + 1));
|
||||
let (data_type, nullable) = projection_metadata(expr, &input_schema)?;
|
||||
let (data_type, nullable) = projection_metadata(expr, &input_schema, &select.from)?;
|
||||
expressions.push(NamedExpr {
|
||||
name: output_name.clone(),
|
||||
expr: planned_expr,
|
||||
@ -112,7 +112,7 @@ pub fn plan_select(
|
||||
};
|
||||
|
||||
let output_schema = plan.output_schema().clone();
|
||||
maybe_apply_sort(plan, output_schema, &select.order_by)
|
||||
maybe_apply_sort(plan, output_schema, &select.order_by, &select.from)
|
||||
}
|
||||
|
||||
fn is_wildcard_projection(items: &[SelectItem]) -> bool {
|
||||
@ -166,23 +166,25 @@ fn plan_from_tables(
|
||||
Ok((plan, combined_schema))
|
||||
}
|
||||
|
||||
fn plan_expr(expr: &Expr, schema: &Schema) -> Result<LogicalExpr, PlannerError> {
|
||||
fn plan_expr(
|
||||
expr: &Expr,
|
||||
schema: &Schema,
|
||||
tables: &[TableRef],
|
||||
) -> Result<LogicalExpr, PlannerError> {
|
||||
match expr {
|
||||
Expr::Identifier(name) => {
|
||||
if schema.index_of(name).is_none() {
|
||||
return Err(PlannerError::UnknownColumn(name.clone()));
|
||||
}
|
||||
Ok(LogicalExpr::Column(name.clone()))
|
||||
let resolved = resolve_column_name(name, schema, tables)?;
|
||||
Ok(LogicalExpr::Column(resolved))
|
||||
}
|
||||
Expr::Literal(literal) => Ok(LogicalExpr::Literal(plan_literal(literal))),
|
||||
Expr::Binary { left, op, right } => match op {
|
||||
BinaryOp::Eq => Ok(LogicalExpr::Eq(
|
||||
Box::new(plan_expr(left, schema)?),
|
||||
Box::new(plan_expr(right, schema)?),
|
||||
Box::new(plan_expr(left, schema, tables)?),
|
||||
Box::new(plan_expr(right, schema, tables)?),
|
||||
)),
|
||||
BinaryOp::And => Ok(LogicalExpr::And(
|
||||
Box::new(plan_expr(left, schema)?),
|
||||
Box::new(plan_expr(right, schema)?),
|
||||
Box::new(plan_expr(left, schema, tables)?),
|
||||
Box::new(plan_expr(right, schema, tables)?),
|
||||
)),
|
||||
},
|
||||
}
|
||||
@ -192,6 +194,7 @@ fn maybe_apply_sort(
|
||||
plan: LogicalPlan,
|
||||
schema: Schema,
|
||||
order_by: &[OrderByItem],
|
||||
tables: &[TableRef],
|
||||
) -> Result<LogicalPlan, PlannerError> {
|
||||
if order_by.is_empty() {
|
||||
return Ok(plan);
|
||||
@ -203,9 +206,7 @@ fn maybe_apply_sort(
|
||||
Expr::Identifier(name) => name.clone(),
|
||||
_ => return Err(PlannerError::UnsupportedOrderBy),
|
||||
};
|
||||
if schema.index_of(&column).is_none() {
|
||||
return Err(PlannerError::UnknownColumn(column));
|
||||
}
|
||||
let column = resolve_column_name(&column, &schema, tables)?;
|
||||
keys.push(SortKey {
|
||||
column,
|
||||
direction: match item.direction {
|
||||
@ -229,11 +230,16 @@ fn plan_literal(literal: &Literal) -> Value {
|
||||
}
|
||||
}
|
||||
|
||||
fn projection_metadata(expr: &Expr, schema: &Schema) -> Result<(DataType, bool), PlannerError> {
|
||||
fn projection_metadata(
|
||||
expr: &Expr,
|
||||
schema: &Schema,
|
||||
tables: &[TableRef],
|
||||
) -> Result<(DataType, bool), PlannerError> {
|
||||
match expr {
|
||||
Expr::Identifier(name) => {
|
||||
let resolved = resolve_column_name(name, schema, tables)?;
|
||||
let index = schema
|
||||
.index_of(name)
|
||||
.index_of(&resolved)
|
||||
.ok_or_else(|| PlannerError::UnknownColumn(name.clone()))?;
|
||||
let field = &schema.fields()[index];
|
||||
Ok((field.data_type().clone(), field.nullable()))
|
||||
@ -244,6 +250,27 @@ fn projection_metadata(expr: &Expr, schema: &Schema) -> Result<(DataType, bool),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_column_name(
|
||||
name: &str,
|
||||
schema: &Schema,
|
||||
tables: &[TableRef],
|
||||
) -> Result<String, PlannerError> {
|
||||
if schema.index_of(name).is_some() {
|
||||
return Ok(name.to_string());
|
||||
}
|
||||
|
||||
if let Some((table_name, column_name)) = name.rsplit_once('.')
|
||||
&& tables.len() == 1
|
||||
&& tables[0].alias.is_none()
|
||||
&& tables[0].name == table_name
|
||||
&& schema.index_of(column_name).is_some()
|
||||
{
|
||||
return Ok(column_name.to_string());
|
||||
}
|
||||
|
||||
Err(PlannerError::UnknownColumn(name.to_string()))
|
||||
}
|
||||
|
||||
fn default_projection_name(expr: &Expr, ordinal: usize) -> String {
|
||||
match expr {
|
||||
Expr::Identifier(name) => name.clone(),
|
||||
@ -423,6 +450,28 @@ mod tests {
|
||||
assert_eq!(schema.fields()[0].name(), "p.parent");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plans_single_table_with_qualified_table_name() {
|
||||
let instance: Instance = vec![Atom::new(
|
||||
"Parent",
|
||||
vec![Term::constant("alice"), Term::constant("bob")],
|
||||
)]
|
||||
.into_iter()
|
||||
.collect();
|
||||
let mut catalog = PredicateCatalog::from_instance(&instance).unwrap();
|
||||
catalog
|
||||
.rename_columns("Parent", ["parent", "child"])
|
||||
.unwrap();
|
||||
|
||||
let select =
|
||||
parse_select("SELECT Parent.parent FROM Parent WHERE Parent.child = 'bob'").unwrap();
|
||||
|
||||
let plan = plan_select(&select, &catalog).unwrap();
|
||||
let schema = plan.output_schema();
|
||||
assert_eq!(schema.len(), 1);
|
||||
assert_eq!(schema.fields()[0].name(), "Parent.parent");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plans_conjunctive_filter() {
|
||||
let instance: Instance = vec![Atom::new(
|
||||
|
||||
@ -101,6 +101,25 @@ fn select_uses_explicit_catalog_column_names() {
|
||||
assert_eq!(format!("{}", result.rows()[0].values()[0]), "alice");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_uses_qualified_table_name_in_single_table_query() {
|
||||
let instance = parent_instance();
|
||||
let mut catalog = PredicateCatalog::from_instance(&instance).unwrap();
|
||||
catalog
|
||||
.rename_columns("Parent", ["parent", "child"])
|
||||
.unwrap();
|
||||
|
||||
let select =
|
||||
parse_select("SELECT Parent.parent FROM Parent WHERE Parent.child = 'bob'").unwrap();
|
||||
|
||||
let plan = plan_select(&select, &catalog).unwrap();
|
||||
let result = execute(&plan, &instance).unwrap();
|
||||
|
||||
assert_eq!(result.schema().fields()[0].name(), "Parent.parent");
|
||||
assert_eq!(result.rows().len(), 1);
|
||||
assert_eq!(format!("{}", result.rows()[0].values()[0]), "alice");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_join_filters_cross_product_by_qualified_columns() {
|
||||
let instance: Instance = vec![
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user