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`).
|
- 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.
|
- 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`.
|
- 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.
|
- 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.
|
- 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.
|
- 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`
|
- default column names are positional such as `c0`, `c1`
|
||||||
- stable names require explicit catalog registration or `schema ...` in the frontend
|
- 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
|
- joins currently use comma-separated tables plus `WHERE` filtering
|
||||||
- multi-table queries require qualified column names such as `Parent.child`
|
- multi-table queries require qualified column names such as `Parent.child`
|
||||||
- table aliases are supported via `FROM Parent AS p`
|
- 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)?;
|
let (mut plan, input_schema) = plan_from_tables(&select.from, catalog)?;
|
||||||
|
|
||||||
if let Some(selection) = &select.selection {
|
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 {
|
plan = LogicalPlan::Filter {
|
||||||
input: Box::new(plan),
|
input: Box::new(plan),
|
||||||
predicate,
|
predicate,
|
||||||
@ -82,7 +82,7 @@ pub fn plan_select(
|
|||||||
|
|
||||||
if is_wildcard_projection(&select.projection) {
|
if is_wildcard_projection(&select.projection) {
|
||||||
let output_schema = plan.output_schema().clone();
|
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();
|
let mut expressions = Vec::new();
|
||||||
@ -90,11 +90,11 @@ pub fn plan_select(
|
|||||||
for (index, item) in select.projection.iter().enumerate() {
|
for (index, item) in select.projection.iter().enumerate() {
|
||||||
match item {
|
match item {
|
||||||
SelectItem::Expr { expr, alias } => {
|
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
|
let output_name = alias
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| default_projection_name(expr, index + 1));
|
.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 {
|
expressions.push(NamedExpr {
|
||||||
name: output_name.clone(),
|
name: output_name.clone(),
|
||||||
expr: planned_expr,
|
expr: planned_expr,
|
||||||
@ -112,7 +112,7 @@ pub fn plan_select(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let output_schema = plan.output_schema().clone();
|
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 {
|
fn is_wildcard_projection(items: &[SelectItem]) -> bool {
|
||||||
@ -166,23 +166,25 @@ fn plan_from_tables(
|
|||||||
Ok((plan, combined_schema))
|
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 {
|
match expr {
|
||||||
Expr::Identifier(name) => {
|
Expr::Identifier(name) => {
|
||||||
if schema.index_of(name).is_none() {
|
let resolved = resolve_column_name(name, schema, tables)?;
|
||||||
return Err(PlannerError::UnknownColumn(name.clone()));
|
Ok(LogicalExpr::Column(resolved))
|
||||||
}
|
|
||||||
Ok(LogicalExpr::Column(name.clone()))
|
|
||||||
}
|
}
|
||||||
Expr::Literal(literal) => Ok(LogicalExpr::Literal(plan_literal(literal))),
|
Expr::Literal(literal) => Ok(LogicalExpr::Literal(plan_literal(literal))),
|
||||||
Expr::Binary { left, op, right } => match op {
|
Expr::Binary { left, op, right } => match op {
|
||||||
BinaryOp::Eq => Ok(LogicalExpr::Eq(
|
BinaryOp::Eq => Ok(LogicalExpr::Eq(
|
||||||
Box::new(plan_expr(left, schema)?),
|
Box::new(plan_expr(left, schema, tables)?),
|
||||||
Box::new(plan_expr(right, schema)?),
|
Box::new(plan_expr(right, schema, tables)?),
|
||||||
)),
|
)),
|
||||||
BinaryOp::And => Ok(LogicalExpr::And(
|
BinaryOp::And => Ok(LogicalExpr::And(
|
||||||
Box::new(plan_expr(left, schema)?),
|
Box::new(plan_expr(left, schema, tables)?),
|
||||||
Box::new(plan_expr(right, schema)?),
|
Box::new(plan_expr(right, schema, tables)?),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -192,6 +194,7 @@ fn maybe_apply_sort(
|
|||||||
plan: LogicalPlan,
|
plan: LogicalPlan,
|
||||||
schema: Schema,
|
schema: Schema,
|
||||||
order_by: &[OrderByItem],
|
order_by: &[OrderByItem],
|
||||||
|
tables: &[TableRef],
|
||||||
) -> Result<LogicalPlan, PlannerError> {
|
) -> Result<LogicalPlan, PlannerError> {
|
||||||
if order_by.is_empty() {
|
if order_by.is_empty() {
|
||||||
return Ok(plan);
|
return Ok(plan);
|
||||||
@ -203,9 +206,7 @@ fn maybe_apply_sort(
|
|||||||
Expr::Identifier(name) => name.clone(),
|
Expr::Identifier(name) => name.clone(),
|
||||||
_ => return Err(PlannerError::UnsupportedOrderBy),
|
_ => return Err(PlannerError::UnsupportedOrderBy),
|
||||||
};
|
};
|
||||||
if schema.index_of(&column).is_none() {
|
let column = resolve_column_name(&column, &schema, tables)?;
|
||||||
return Err(PlannerError::UnknownColumn(column));
|
|
||||||
}
|
|
||||||
keys.push(SortKey {
|
keys.push(SortKey {
|
||||||
column,
|
column,
|
||||||
direction: match item.direction {
|
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 {
|
match expr {
|
||||||
Expr::Identifier(name) => {
|
Expr::Identifier(name) => {
|
||||||
|
let resolved = resolve_column_name(name, schema, tables)?;
|
||||||
let index = schema
|
let index = schema
|
||||||
.index_of(name)
|
.index_of(&resolved)
|
||||||
.ok_or_else(|| PlannerError::UnknownColumn(name.clone()))?;
|
.ok_or_else(|| PlannerError::UnknownColumn(name.clone()))?;
|
||||||
let field = &schema.fields()[index];
|
let field = &schema.fields()[index];
|
||||||
Ok((field.data_type().clone(), field.nullable()))
|
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 {
|
fn default_projection_name(expr: &Expr, ordinal: usize) -> String {
|
||||||
match expr {
|
match expr {
|
||||||
Expr::Identifier(name) => name.clone(),
|
Expr::Identifier(name) => name.clone(),
|
||||||
@ -423,6 +450,28 @@ mod tests {
|
|||||||
assert_eq!(schema.fields()[0].name(), "p.parent");
|
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]
|
#[test]
|
||||||
fn plans_conjunctive_filter() {
|
fn plans_conjunctive_filter() {
|
||||||
let instance: Instance = vec![Atom::new(
|
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");
|
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]
|
#[test]
|
||||||
fn select_join_filters_cross_product_by_qualified_columns() {
|
fn select_join_filters_cross_product_by_qualified_columns() {
|
||||||
let instance: Instance = vec![
|
let instance: Instance = vec![
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user