Fix single-table aliases and empty schema tables
This commit is contained in:
parent
964a0d8308
commit
7111a682ff
@ -72,7 +72,7 @@ Quick examples:
|
|||||||
- New chase variants should be composable with existing infrastructure.
|
- New chase variants should be composable with existing infrastructure.
|
||||||
- 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; 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`.
|
||||||
- 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.
|
||||||
|
|||||||
@ -137,8 +137,12 @@ In the REPL or script runner, use the `sql` command and end the statement with
|
|||||||
sql SELECT c0 FROM Parent WHERE c1 = 'bob';
|
sql SELECT c0 FROM Parent WHERE c1 = 'bob';
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`fact`, `rule`, `schema`, `sql`, `query`, and `explain` commands may also span
|
||||||
|
multiple lines in `.ech` scripts as long as the final line ends with the normal
|
||||||
|
terminator.
|
||||||
|
|
||||||
You can also register stable column names for a predicate-backed table in the
|
You can also register stable column names for a predicate-backed table in the
|
||||||
frontend before running SQL:
|
frontend before running SQL, including tables that currently have no facts:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
schema Parent(parent, child).
|
schema Parent(parent, child).
|
||||||
|
|||||||
@ -3,13 +3,13 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use crate::catalog::PredicateCatalog;
|
use crate::catalog::{CatalogError, PredicateCatalog};
|
||||||
use crate::chase::{
|
use crate::chase::{
|
||||||
Atom, Instance, MaterializedState, Rule, Substitution, find_matches, materialize,
|
Atom, Instance, MaterializedState, Rule, Substitution, find_matches, materialize,
|
||||||
};
|
};
|
||||||
use crate::execution::execute;
|
use crate::execution::execute;
|
||||||
use crate::planner::sql::plan_select;
|
use crate::planner::sql::plan_select;
|
||||||
use crate::relational::ResultSet;
|
use crate::relational::{DataType, Field, ResultSet, Schema};
|
||||||
|
|
||||||
use super::language::{Command, parse_script};
|
use super::language::{Command, parse_script};
|
||||||
use super::provenance::explain_atom;
|
use super::provenance::explain_atom;
|
||||||
@ -143,10 +143,25 @@ impl Session {
|
|||||||
let mut catalog =
|
let mut catalog =
|
||||||
PredicateCatalog::from_instance(instance).map_err(|err| err.to_string())?;
|
PredicateCatalog::from_instance(instance).map_err(|err| err.to_string())?;
|
||||||
for (table, columns) in &self.column_names {
|
for (table, columns) in &self.column_names {
|
||||||
|
match catalog.schema_for(table) {
|
||||||
|
Ok(_) => {
|
||||||
catalog
|
catalog
|
||||||
.rename_columns(table, columns.clone())
|
.rename_columns(table, columns.clone())
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
}
|
}
|
||||||
|
Err(CatalogError::UnknownTable(_)) => {
|
||||||
|
let schema = Schema::new(
|
||||||
|
columns
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|name| Field::new(name, DataType::Text, false))
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
catalog.register_table(table.clone(), schema);
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
let plan = plan_select(select, &catalog).map_err(|err| err.to_string())?;
|
let plan = plan_select(select, &catalog).map_err(|err| err.to_string())?;
|
||||||
let result = execute(&plan, instance).map_err(|err| err.to_string())?;
|
let result = execute(&plan, instance).map_err(|err| err.to_string())?;
|
||||||
Ok(render_result_set(&result))
|
Ok(render_result_set(&result))
|
||||||
@ -401,6 +416,21 @@ mod tests {
|
|||||||
assert!(output.contains("alice"));
|
assert!(output.contains("alice"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_runs_sql_query_with_declared_empty_table() {
|
||||||
|
let mut session = Session::new();
|
||||||
|
let output = session
|
||||||
|
.execute_script(
|
||||||
|
"schema Parent(parent, child).\n\
|
||||||
|
sql SELECT parent FROM Parent;",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(output.contains("Registered schema for Parent: parent, child"));
|
||||||
|
assert!(output.contains("0 row(s)"));
|
||||||
|
assert!(output.contains("parent"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn session_runs_sql_join_query() {
|
fn session_runs_sql_join_query() {
|
||||||
let mut session = Session::new();
|
let mut session = Session::new();
|
||||||
@ -443,6 +473,22 @@ mod tests {
|
|||||||
assert!(output.contains("bob | dave"));
|
assert!(output.contains("bob | dave"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_runs_sql_query_with_single_table_alias() {
|
||||||
|
let mut session = Session::new();
|
||||||
|
let output = session
|
||||||
|
.execute_script(
|
||||||
|
"fact Parent(alice, bob).\n\
|
||||||
|
schema Parent(parent, child).\n\
|
||||||
|
sql SELECT p.parent FROM Parent AS p WHERE p.child = 'bob';",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(output.contains("1 row(s)"));
|
||||||
|
assert!(output.contains("p.parent"));
|
||||||
|
assert!(output.contains("alice"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn session_runs_sql_query_with_and_filter() {
|
fn session_runs_sql_query_with_and_filter() {
|
||||||
let mut session = Session::new();
|
let mut session = Session::new();
|
||||||
|
|||||||
@ -125,7 +125,8 @@ fn plan_from_tables(
|
|||||||
return Err(PlannerError::DuplicateSourceName(first_name));
|
return Err(PlannerError::DuplicateSourceName(first_name));
|
||||||
}
|
}
|
||||||
|
|
||||||
let first_schema = input_schema_for_table(first, catalog, tables.len() > 1)?;
|
let first_schema =
|
||||||
|
input_schema_for_table(first, catalog, should_qualify_columns(first, tables))?;
|
||||||
let mut plan = LogicalPlan::Scan {
|
let mut plan = LogicalPlan::Scan {
|
||||||
table: first.name.clone(),
|
table: first.name.clone(),
|
||||||
schema: first_schema.clone(),
|
schema: first_schema.clone(),
|
||||||
@ -138,7 +139,8 @@ fn plan_from_tables(
|
|||||||
return Err(PlannerError::DuplicateSourceName(qualified_name));
|
return Err(PlannerError::DuplicateSourceName(qualified_name));
|
||||||
}
|
}
|
||||||
|
|
||||||
let right_schema = input_schema_for_table(table, catalog, tables.len() > 1)?;
|
let right_schema =
|
||||||
|
input_schema_for_table(table, catalog, should_qualify_columns(table, tables))?;
|
||||||
let join_schema = combine_schemas(&combined_schema, &right_schema);
|
let join_schema = combine_schemas(&combined_schema, &right_schema);
|
||||||
let right_plan = LogicalPlan::Scan {
|
let right_plan = LogicalPlan::Scan {
|
||||||
table: table.name.clone(),
|
table: table.name.clone(),
|
||||||
@ -266,6 +268,10 @@ fn input_schema_for_table(
|
|||||||
Ok(Schema::new(fields))
|
Ok(Schema::new(fields))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_qualify_columns(table: &TableRef, tables: &[TableRef]) -> bool {
|
||||||
|
table.alias.is_some() || tables.len() > 1
|
||||||
|
}
|
||||||
|
|
||||||
fn source_name(table: &TableRef) -> String {
|
fn source_name(table: &TableRef) -> String {
|
||||||
table.alias.clone().unwrap_or_else(|| table.name.clone())
|
table.alias.clone().unwrap_or_else(|| table.name.clone())
|
||||||
}
|
}
|
||||||
@ -386,6 +392,28 @@ mod tests {
|
|||||||
assert_eq!(schema.fields()[1].name(), "q.child");
|
assert_eq!(schema.fields()[1].name(), "q.child");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plans_single_table_with_alias() {
|
||||||
|
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 p.parent FROM Parent AS p WHERE p.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(), "p.parent");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn plans_conjunctive_filter() {
|
fn plans_conjunctive_filter() {
|
||||||
let instance: Instance = vec![Atom::new(
|
let instance: Instance = vec![Atom::new(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user