Compare commits
No commits in common. "23750fec048598af87ec3cac4695edd12833cb16" and "964a0d830844a9008aa1b19239166ca7256b6205" have entirely different histories.
23750fec04
...
964a0d8308
@ -71,9 +71,8 @@ Quick examples:
|
|||||||
- The chase engine should remain largely stateless; pass execution state explicitly.
|
- The chase engine should remain largely stateless; pass execution state explicitly.
|
||||||
- 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-LIMIT` over predicate-backed tables; equality and inequality predicates combined with `AND` and `OR`; comma-join style multi-table queries; table aliases; ordering by output-column names; integer and string literals.
|
- 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; 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.
|
||||||
|
|||||||
21
README.md
21
README.md
@ -14,7 +14,7 @@ execution boundaries.
|
|||||||
- Provenance-oriented explanations for derived answers
|
- Provenance-oriented explanations for derived answers
|
||||||
- Script, REPL, and local web UI for experimentation
|
- Script, REPL, and local web UI for experimentation
|
||||||
- Relational schema, catalog, logical-plan, and execution scaffolding
|
- Relational schema, catalog, logical-plan, and execution scaffolding
|
||||||
- A minimal SQL slice for `SELECT-FROM-WHERE-ORDER BY-LIMIT` queries over predicate-backed tables
|
- A minimal SQL slice for `SELECT-FROM-WHERE-ORDER BY` queries over predicate-backed tables
|
||||||
|
|
||||||
### Architecture
|
### Architecture
|
||||||
|
|
||||||
@ -111,7 +111,7 @@ The repository now has a narrow SQL pipeline with:
|
|||||||
- relational schemas, rows, and values
|
- relational schemas, rows, and values
|
||||||
- SQL parsing for a small subset
|
- SQL parsing for a small subset
|
||||||
- logical planning
|
- logical planning
|
||||||
- execution for filtering, ordering, limiting, and basic multi-table joins
|
- execution for filtering, ordering, and basic multi-table joins
|
||||||
|
|
||||||
Currently supported examples:
|
Currently supported examples:
|
||||||
|
|
||||||
@ -119,12 +119,9 @@ Currently supported examples:
|
|||||||
SELECT * FROM Parent
|
SELECT * FROM Parent
|
||||||
SELECT c0 FROM Parent
|
SELECT c0 FROM Parent
|
||||||
SELECT c0 FROM Parent WHERE c1 = 'bob'
|
SELECT c0 FROM Parent WHERE c1 = 'bob'
|
||||||
SELECT c0 FROM Parent WHERE c1 != 'bob'
|
|
||||||
SELECT c0 FROM Parent WHERE c1 = 'bob' AND c0 = 'alice'
|
SELECT c0 FROM Parent WHERE c1 = 'bob' AND c0 = 'alice'
|
||||||
SELECT c0 FROM Parent WHERE c1 = 'bob' OR c1 = 'carol'
|
|
||||||
SELECT c0 FROM Parent ORDER BY c0 DESC
|
SELECT c0 FROM Parent ORDER BY c0 DESC
|
||||||
SELECT c0 FROM Parent ORDER BY c0 ASC LIMIT 1
|
SELECT c0 AS parent_name, 'seed' AS label FROM Parent
|
||||||
SELECT c0 AS parent_name, 'seed' AS label, 42 AS answer FROM Parent
|
|
||||||
SELECT Parent.parent, Ancestor.child
|
SELECT Parent.parent, Ancestor.child
|
||||||
FROM Parent, Ancestor
|
FROM Parent, Ancestor
|
||||||
WHERE Parent.child = Ancestor.parent
|
WHERE Parent.child = Ancestor.parent
|
||||||
@ -140,12 +137,8 @@ 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, including tables that currently have no facts:
|
frontend before running SQL:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
schema Parent(parent, child).
|
schema Parent(parent, child).
|
||||||
@ -171,14 +164,11 @@ 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`
|
||||||
- `WHERE` supports `=`, `!=`/`<>`, `AND`, and `OR` (with standard precedence)
|
- `WHERE` supports equality predicates combined with `AND`
|
||||||
- `ORDER BY` supports output-column ordering with `ASC`/`DESC`
|
- `ORDER BY` supports output-column ordering with `ASC`/`DESC`
|
||||||
- `LIMIT` restricts the number of output rows
|
|
||||||
- literals include strings, integers, and `NULL`
|
|
||||||
- no aggregates
|
- no aggregates
|
||||||
- projection aliases only via `AS`
|
- projection aliases only via `AS`
|
||||||
|
|
||||||
@ -188,7 +178,6 @@ Runnable SQL examples:
|
|||||||
- `examples/scripts/sql_join.ech`
|
- `examples/scripts/sql_join.ech`
|
||||||
- `examples/scripts/sql_self_join.ech`
|
- `examples/scripts/sql_self_join.ech`
|
||||||
- `examples/scripts/sql_order_by.ech`
|
- `examples/scripts/sql_order_by.ech`
|
||||||
- `examples/scripts/sql_filter_ops.ech`
|
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
|
|||||||
@ -28,13 +28,10 @@ This document tracks the current state and next steps for the repository.
|
|||||||
- [x] Minimal SQL AST and parser
|
- [x] Minimal SQL AST and parser
|
||||||
- [x] Logical plan scaffolding
|
- [x] Logical plan scaffolding
|
||||||
- [x] Logical-plan execution for the first SQL slice
|
- [x] Logical-plan execution for the first SQL slice
|
||||||
- [x] `SELECT-FROM-WHERE-ORDER BY-LIMIT` support with positional or named columns
|
- [x] `SELECT-FROM-WHERE-ORDER BY` support with positional or named columns
|
||||||
- [x] Basic multi-table SQL joins via qualified-column filtering
|
- [x] Basic multi-table SQL joins via qualified-column filtering
|
||||||
- [x] Table aliases for self-joins and qualified references
|
- [x] Table aliases for self-joins and qualified references
|
||||||
- [x] Basic `ORDER BY` support over output columns
|
- [x] Basic `ORDER BY` support over output columns
|
||||||
- [x] `!=`/`<>` inequality and `OR` disjunction in `WHERE` clauses
|
|
||||||
- [x] `LIMIT` clause for restricting output row count
|
|
||||||
- [x] Integer literal and `DataType::Integer` support
|
|
||||||
|
|
||||||
### Near-Term Cleanup
|
### Near-Term Cleanup
|
||||||
|
|
||||||
@ -75,7 +72,7 @@ This document tracks the current state and next steps for the repository.
|
|||||||
|
|
||||||
- [x] Restricted chase
|
- [x] Restricted chase
|
||||||
- [x] Standard chase
|
- [x] Standard chase
|
||||||
- [x] Oblivious chase
|
- [ ] Oblivious chase
|
||||||
- [ ] Skolem chase
|
- [ ] Skolem chase
|
||||||
- [ ] Core chase
|
- [ ] Core chase
|
||||||
- [ ] Negative constraints
|
- [ ] Negative constraints
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
# Demonstrate inequality, OR, LIMIT, and integer literals in the SQL frontend.
|
|
||||||
|
|
||||||
fact Employee(alice, 30, engineering).
|
|
||||||
fact Employee(bob, 25, sales).
|
|
||||||
fact Employee(carol, 35, engineering).
|
|
||||||
fact Employee(dave, 28, marketing).
|
|
||||||
|
|
||||||
schema Employee(name, age, dept).
|
|
||||||
|
|
||||||
# Inequality: exclude engineering.
|
|
||||||
sql SELECT name FROM Employee WHERE dept != 'engineering';
|
|
||||||
|
|
||||||
# OR: engineering or marketing.
|
|
||||||
sql SELECT name, dept FROM Employee WHERE dept = 'engineering' OR dept = 'marketing';
|
|
||||||
|
|
||||||
# LIMIT: first two rows in name order.
|
|
||||||
sql SELECT name FROM Employee ORDER BY name ASC LIMIT 2;
|
|
||||||
|
|
||||||
# Integer literal in projection.
|
|
||||||
sql SELECT name, 1 AS active FROM Employee WHERE dept = 'sales';
|
|
||||||
@ -64,12 +64,6 @@ pub enum ChaseVariant {
|
|||||||
/// rule with the same frontier variable bindings. This is the default.
|
/// rule with the same frontier variable bindings. This is the default.
|
||||||
#[default]
|
#[default]
|
||||||
Restricted,
|
Restricted,
|
||||||
/// Oblivious chase: fires every matching rule application without checking
|
|
||||||
/// head satisfaction or tracking triggers. Terminates only when no body
|
|
||||||
/// match produces a genuinely new fact. For rules with existential
|
|
||||||
/// variables this variant will typically not terminate (it will hit the
|
|
||||||
/// step limit) because each application generates fresh nulls.
|
|
||||||
Oblivious,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration for the chase algorithm.
|
/// Configuration for the chase algorithm.
|
||||||
@ -118,26 +112,6 @@ pub fn standard_chase(instance: Instance, rules: &[Rule]) -> ChaseResult {
|
|||||||
chase_with_config(instance, rules, config)
|
chase_with_config(instance, rules, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the oblivious chase algorithm.
|
|
||||||
///
|
|
||||||
/// The oblivious chase fires every rule application whose body matches,
|
|
||||||
/// without checking whether the head is already satisfied and without
|
|
||||||
/// tracking previously applied triggers. It terminates only when a full
|
|
||||||
/// round produces no new facts.
|
|
||||||
///
|
|
||||||
/// For Datalog rules (no existential variables) the oblivious chase
|
|
||||||
/// reaches the same fixpoint as the restricted and standard variants.
|
|
||||||
/// For rules with existential variables it will typically not terminate
|
|
||||||
/// because each application generates fresh labeled nulls; in that case
|
|
||||||
/// it will run until the step limit.
|
|
||||||
pub fn oblivious_chase(instance: Instance, rules: &[Rule]) -> ChaseResult {
|
|
||||||
let config = ChaseConfig {
|
|
||||||
variant: ChaseVariant::Oblivious,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
chase_with_config(instance, rules, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run the chase with custom configuration.
|
/// Run the chase with custom configuration.
|
||||||
pub fn chase_with_config(
|
pub fn chase_with_config(
|
||||||
mut instance: Instance,
|
mut instance: Instance,
|
||||||
@ -163,7 +137,6 @@ pub fn chase_with_config(
|
|||||||
ChaseVariant::Restricted => {
|
ChaseVariant::Restricted => {
|
||||||
restricted_chase_step(&instance, rules, &mut null_gen, &mut applied_triggers)
|
restricted_chase_step(&instance, rules, &mut null_gen, &mut applied_triggers)
|
||||||
}
|
}
|
||||||
ChaseVariant::Oblivious => oblivious_chase_step(&instance, rules, &mut null_gen),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if new_facts.is_empty() {
|
if new_facts.is_empty() {
|
||||||
@ -258,32 +231,6 @@ fn restricted_chase_step(
|
|||||||
new_facts
|
new_facts
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform a single oblivious chase step: fire all matching rule applications
|
|
||||||
/// without checking head satisfaction or tracking triggers.
|
|
||||||
fn oblivious_chase_step(
|
|
||||||
instance: &Instance,
|
|
||||||
rules: &[Rule],
|
|
||||||
null_gen: &mut NullGenerator,
|
|
||||||
) -> Vec<Atom> {
|
|
||||||
let mut new_facts = Vec::new();
|
|
||||||
|
|
||||||
for rule in rules {
|
|
||||||
let matches = find_matches(instance, &rule.body);
|
|
||||||
|
|
||||||
for subst in matches {
|
|
||||||
let derived = apply_rule_head(rule, &subst, null_gen);
|
|
||||||
|
|
||||||
for fact in derived {
|
|
||||||
if !instance.contains(&fact) {
|
|
||||||
new_facts.push(fact);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
new_facts
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A trigger for EGD applications, tracking which EGD was applied with which body bindings.
|
/// A trigger for EGD applications, tracking which EGD was applied with which body bindings.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
struct EgdTrigger {
|
struct EgdTrigger {
|
||||||
@ -382,7 +329,6 @@ pub fn chase_full(
|
|||||||
ChaseVariant::Restricted => {
|
ChaseVariant::Restricted => {
|
||||||
restricted_chase_step(&instance, tgds, &mut null_gen, &mut applied_triggers)
|
restricted_chase_step(&instance, tgds, &mut null_gen, &mut applied_triggers)
|
||||||
}
|
}
|
||||||
ChaseVariant::Oblivious => oblivious_chase_step(&instance, tgds, &mut null_gen),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let tgd_changes = !new_facts.is_empty();
|
let tgd_changes = !new_facts.is_empty();
|
||||||
@ -850,117 +796,4 @@ mod tests {
|
|||||||
assert!(result.error.is_none());
|
assert!(result.error.is_none());
|
||||||
assert_eq!(result.instance.facts_for_predicate("B").len(), 1);
|
assert_eq!(result.instance.facts_for_predicate("B").len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Oblivious chase tests
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_oblivious_chase_datalog_rules() {
|
|
||||||
let instance: Instance = vec![
|
|
||||||
Atom::new(
|
|
||||||
"Parent",
|
|
||||||
vec![Term::constant("alice"), Term::constant("bob")],
|
|
||||||
),
|
|
||||||
Atom::new(
|
|
||||||
"Parent",
|
|
||||||
vec![Term::constant("bob"), Term::constant("carol")],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let rule1 = RuleBuilder::new()
|
|
||||||
.when("Parent", vec![Term::var("X"), Term::var("Y")])
|
|
||||||
.then("Ancestor", vec![Term::var("X"), Term::var("Y")])
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let rule2 = RuleBuilder::new()
|
|
||||||
.when("Ancestor", vec![Term::var("X"), Term::var("Y")])
|
|
||||||
.when("Parent", vec![Term::var("Y"), Term::var("Z")])
|
|
||||||
.then("Ancestor", vec![Term::var("X"), Term::var("Z")])
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let result = oblivious_chase(instance, &[rule1, rule2]);
|
|
||||||
|
|
||||||
assert!(result.terminated);
|
|
||||||
let ancestors = result.instance.facts_for_predicate("Ancestor");
|
|
||||||
assert_eq!(ancestors.len(), 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_oblivious_chase_matches_restricted_for_datalog() {
|
|
||||||
let instance: Instance = vec![
|
|
||||||
Atom::new("Edge", vec![Term::constant("a"), Term::constant("b")]),
|
|
||||||
Atom::new("Edge", vec![Term::constant("b"), Term::constant("c")]),
|
|
||||||
Atom::new("Edge", vec![Term::constant("c"), Term::constant("d")]),
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let rule1 = RuleBuilder::new()
|
|
||||||
.when("Edge", vec![Term::var("X"), Term::var("Y")])
|
|
||||||
.then("Path", vec![Term::var("X"), Term::var("Y")])
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let rule2 = RuleBuilder::new()
|
|
||||||
.when("Path", vec![Term::var("X"), Term::var("Y")])
|
|
||||||
.when("Edge", vec![Term::var("Y"), Term::var("Z")])
|
|
||||||
.then("Path", vec![Term::var("X"), Term::var("Z")])
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let rules = vec![rule1, rule2];
|
|
||||||
|
|
||||||
let oblivious_result = oblivious_chase(instance.clone(), &rules);
|
|
||||||
let restricted_result = chase(instance, &rules);
|
|
||||||
|
|
||||||
assert!(oblivious_result.terminated);
|
|
||||||
assert!(restricted_result.terminated);
|
|
||||||
|
|
||||||
let oblivious_paths = oblivious_result.instance.facts_for_predicate("Path");
|
|
||||||
let restricted_paths = restricted_result.instance.facts_for_predicate("Path");
|
|
||||||
assert_eq!(oblivious_paths.len(), restricted_paths.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_oblivious_chase_does_not_terminate_with_existentials() {
|
|
||||||
let instance: Instance = vec![Atom::new("Person", vec![Term::constant("alice")])]
|
|
||||||
.into_iter()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let rule = RuleBuilder::new()
|
|
||||||
.when("Person", vec![Term::var("X")])
|
|
||||||
.then("HasSSN", vec![Term::var("X"), Term::var("Y")])
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let config = ChaseConfig {
|
|
||||||
max_steps: 10,
|
|
||||||
variant: ChaseVariant::Oblivious,
|
|
||||||
};
|
|
||||||
let result = chase_with_config(instance, &[rule], config);
|
|
||||||
|
|
||||||
// The oblivious chase generates a fresh null each round, so it
|
|
||||||
// should hit the step limit rather than reaching a fixpoint.
|
|
||||||
assert!(!result.terminated);
|
|
||||||
assert!(result.instance.facts_for_predicate("HasSSN").len() > 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_oblivious_chase_via_config() {
|
|
||||||
let instance: Instance = vec![Atom::new("A", vec![Term::constant("x")])]
|
|
||||||
.into_iter()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let rule = RuleBuilder::new()
|
|
||||||
.when("A", vec![Term::var("X")])
|
|
||||||
.then("B", vec![Term::var("X")])
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let config = ChaseConfig {
|
|
||||||
variant: ChaseVariant::Oblivious,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let result = chase_with_config(instance, &[rule], config);
|
|
||||||
|
|
||||||
assert!(result.terminated);
|
|
||||||
assert_eq!(result.instance.facts_for_predicate("B").len(), 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ mod engine;
|
|||||||
pub use atom::Atom;
|
pub use atom::Atom;
|
||||||
pub use engine::{
|
pub use engine::{
|
||||||
ChaseConfig, ChaseError, ChaseResult, ChaseVariant, chase, chase_full, chase_with_config,
|
ChaseConfig, ChaseError, ChaseResult, ChaseVariant, chase, chase_full, chase_with_config,
|
||||||
chase_with_egds, oblivious_chase, standard_chase,
|
chase_with_egds, standard_chase,
|
||||||
};
|
};
|
||||||
pub use inference::{Derivation, MaterializedState, find_matches, materialize};
|
pub use inference::{Derivation, MaterializedState, find_matches, materialize};
|
||||||
pub use instance::{Instance, InstanceError};
|
pub use instance::{Instance, InstanceError};
|
||||||
|
|||||||
@ -101,11 +101,6 @@ pub fn execute(plan: &LogicalPlan, instance: &Instance) -> Result<ResultSet, Exe
|
|||||||
rows.sort_by(|left, right| compare_rows(left, right, &resolved_keys));
|
rows.sort_by(|left, right| compare_rows(left, right, &resolved_keys));
|
||||||
Ok(ResultSet::new(schema.clone(), rows))
|
Ok(ResultSet::new(schema.clone(), rows))
|
||||||
}
|
}
|
||||||
LogicalPlan::Limit { input, count } => {
|
|
||||||
let result = execute(input, instance)?;
|
|
||||||
let rows = result.rows().iter().take(*count).cloned().collect();
|
|
||||||
Ok(ResultSet::new(result.schema().clone(), rows))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,16 +113,9 @@ fn eval_predicate(
|
|||||||
LogicalExpr::Eq(left, right) => Ok(eval_expr(left, row, schema)?
|
LogicalExpr::Eq(left, right) => Ok(eval_expr(left, row, schema)?
|
||||||
.sql_eq(&eval_expr(right, row, schema)?)
|
.sql_eq(&eval_expr(right, row, schema)?)
|
||||||
.unwrap_or(false)),
|
.unwrap_or(false)),
|
||||||
LogicalExpr::Ne(left, right) => Ok(eval_expr(left, row, schema)?
|
|
||||||
.sql_eq(&eval_expr(right, row, schema)?)
|
|
||||||
.map(|eq| !eq)
|
|
||||||
.unwrap_or(false)),
|
|
||||||
LogicalExpr::And(left, right) => {
|
LogicalExpr::And(left, right) => {
|
||||||
Ok(eval_predicate(left, row, schema)? && eval_predicate(right, row, schema)?)
|
Ok(eval_predicate(left, row, schema)? && eval_predicate(right, row, schema)?)
|
||||||
}
|
}
|
||||||
LogicalExpr::Or(left, right) => {
|
|
||||||
Ok(eval_predicate(left, row, schema)? || eval_predicate(right, row, schema)?)
|
|
||||||
}
|
|
||||||
_ => Ok(false),
|
_ => Ok(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -150,19 +138,9 @@ fn eval_expr(
|
|||||||
let right = eval_expr(right, row, schema)?;
|
let right = eval_expr(right, row, schema)?;
|
||||||
Ok(Value::Boolean(left.sql_eq(&right).unwrap_or(false)))
|
Ok(Value::Boolean(left.sql_eq(&right).unwrap_or(false)))
|
||||||
}
|
}
|
||||||
LogicalExpr::Ne(left, right) => {
|
|
||||||
let left = eval_expr(left, row, schema)?;
|
|
||||||
let right = eval_expr(right, row, schema)?;
|
|
||||||
Ok(Value::Boolean(
|
|
||||||
left.sql_eq(&right).map(|eq| !eq).unwrap_or(false),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
LogicalExpr::And(left, right) => Ok(Value::Boolean(
|
LogicalExpr::And(left, right) => Ok(Value::Boolean(
|
||||||
eval_predicate(left, row, schema)? && eval_predicate(right, row, schema)?,
|
eval_predicate(left, row, schema)? && eval_predicate(right, row, schema)?,
|
||||||
)),
|
)),
|
||||||
LogicalExpr::Or(left, right) => Ok(Value::Boolean(
|
|
||||||
eval_predicate(left, row, schema)? || eval_predicate(right, row, schema)?,
|
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,11 +188,7 @@ fn compare_values(left: &Value, right: &Value) -> Ordering {
|
|||||||
(Value::Null, _) => Ordering::Greater,
|
(Value::Null, _) => Ordering::Greater,
|
||||||
(_, Value::Null) => Ordering::Less,
|
(_, Value::Null) => Ordering::Less,
|
||||||
(Value::Text(left), Value::Text(right)) => left.cmp(right),
|
(Value::Text(left), Value::Text(right)) => left.cmp(right),
|
||||||
(Value::Integer(left), Value::Integer(right)) => left.cmp(right),
|
|
||||||
(Value::Boolean(left), Value::Boolean(right)) => left.cmp(right),
|
(Value::Boolean(left), Value::Boolean(right)) => left.cmp(right),
|
||||||
// Cross-type ordering: Integer < Text < Boolean
|
|
||||||
(Value::Integer(_), _) => Ordering::Less,
|
|
||||||
(_, Value::Integer(_)) => Ordering::Greater,
|
|
||||||
(Value::Text(_), Value::Boolean(_)) => Ordering::Less,
|
(Value::Text(_), Value::Boolean(_)) => Ordering::Less,
|
||||||
(Value::Boolean(_), Value::Text(_)) => Ordering::Greater,
|
(Value::Boolean(_), Value::Text(_)) => Ordering::Greater,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,13 +3,13 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use crate::catalog::{CatalogError, PredicateCatalog};
|
use crate::catalog::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::{DataType, Field, ResultSet, Schema};
|
use crate::relational::ResultSet;
|
||||||
|
|
||||||
use super::language::{Command, parse_script};
|
use super::language::{Command, parse_script};
|
||||||
use super::provenance::explain_atom;
|
use super::provenance::explain_atom;
|
||||||
@ -143,25 +143,10 @@ 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))
|
||||||
@ -416,21 +401,6 @@ 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();
|
||||||
@ -473,22 +443,6 @@ 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();
|
||||||
|
|||||||
@ -18,5 +18,5 @@ pub mod sql;
|
|||||||
// Lower-level reasoning and provenance APIs remain under `query_engine::chase`.
|
// Lower-level reasoning and provenance APIs remain under `query_engine::chase`.
|
||||||
pub use chase::{
|
pub use chase::{
|
||||||
Atom, ChaseConfig, ChaseError, ChaseResult, ChaseVariant, Instance, Rule, RuleBuilder, Term,
|
Atom, ChaseConfig, ChaseError, ChaseResult, ChaseVariant, Instance, Rule, RuleBuilder, Term,
|
||||||
chase, chase_with_config, oblivious_chase, standard_chase,
|
chase, chase_with_config, standard_chase,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -18,12 +18,8 @@ pub enum LogicalExpr {
|
|||||||
Literal(Value),
|
Literal(Value),
|
||||||
/// Equality.
|
/// Equality.
|
||||||
Eq(Box<LogicalExpr>, Box<LogicalExpr>),
|
Eq(Box<LogicalExpr>, Box<LogicalExpr>),
|
||||||
/// Inequality.
|
|
||||||
Ne(Box<LogicalExpr>, Box<LogicalExpr>),
|
|
||||||
/// Boolean conjunction.
|
/// Boolean conjunction.
|
||||||
And(Box<LogicalExpr>, Box<LogicalExpr>),
|
And(Box<LogicalExpr>, Box<LogicalExpr>),
|
||||||
/// Boolean disjunction.
|
|
||||||
Or(Box<LogicalExpr>, Box<LogicalExpr>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A named output expression in a projection.
|
/// A named output expression in a projection.
|
||||||
@ -72,11 +68,6 @@ pub enum LogicalPlan {
|
|||||||
expressions: Vec<NamedExpr>,
|
expressions: Vec<NamedExpr>,
|
||||||
schema: Schema,
|
schema: Schema,
|
||||||
},
|
},
|
||||||
/// Limit the number of output rows.
|
|
||||||
Limit {
|
|
||||||
input: Box<LogicalPlan>,
|
|
||||||
count: usize,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LogicalPlan {
|
impl LogicalPlan {
|
||||||
@ -88,7 +79,6 @@ impl LogicalPlan {
|
|||||||
Self::Filter { input, .. } => input.output_schema(),
|
Self::Filter { input, .. } => input.output_schema(),
|
||||||
Self::Sort { schema, .. } => schema,
|
Self::Sort { schema, .. } => schema,
|
||||||
Self::Project { schema, .. } => schema,
|
Self::Project { schema, .. } => schema,
|
||||||
Self::Limit { input, .. } => input.output_schema(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,8 +22,6 @@ pub enum PlannerError {
|
|||||||
DuplicateSourceName(String),
|
DuplicateSourceName(String),
|
||||||
/// The current `ORDER BY` subset only supports output column names.
|
/// The current `ORDER BY` subset only supports output column names.
|
||||||
UnsupportedOrderBy,
|
UnsupportedOrderBy,
|
||||||
/// The parser or AST contains a wildcard mixed with other projection items.
|
|
||||||
MixedWildcardProjection,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for PlannerError {
|
impl fmt::Display for PlannerError {
|
||||||
@ -37,12 +35,6 @@ impl fmt::Display for PlannerError {
|
|||||||
Self::UnsupportedOrderBy => {
|
Self::UnsupportedOrderBy => {
|
||||||
write!(f, "only output column names are supported in ORDER BY")
|
write!(f, "only output column names are supported in ORDER BY")
|
||||||
}
|
}
|
||||||
Self::MixedWildcardProjection => {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"wildcard projections cannot be combined with other items"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,10 +43,9 @@ impl Error for PlannerError {
|
|||||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||||
match self {
|
match self {
|
||||||
Self::Catalog(err) => Some(err),
|
Self::Catalog(err) => Some(err),
|
||||||
Self::UnknownColumn(_)
|
Self::UnknownColumn(_) | Self::DuplicateSourceName(_) | Self::UnsupportedOrderBy => {
|
||||||
| Self::DuplicateSourceName(_)
|
None
|
||||||
| Self::UnsupportedOrderBy
|
}
|
||||||
| Self::MixedWildcardProjection => None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,53 +64,46 @@ 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, &select.from)?;
|
let predicate = plan_expr(selection, &input_schema)?;
|
||||||
plan = LogicalPlan::Filter {
|
plan = LogicalPlan::Filter {
|
||||||
input: Box::new(plan),
|
input: Box::new(plan),
|
||||||
predicate,
|
predicate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if !is_wildcard_projection(&select.projection) {
|
if is_wildcard_projection(&select.projection) {
|
||||||
|
let output_schema = plan.output_schema().clone();
|
||||||
|
return maybe_apply_sort(plan, output_schema, &select.order_by);
|
||||||
|
}
|
||||||
|
|
||||||
let mut expressions = Vec::new();
|
let mut expressions = Vec::new();
|
||||||
let mut fields = Vec::new();
|
let mut fields = Vec::new();
|
||||||
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, &select.from)?;
|
let planned_expr = plan_expr(expr, &input_schema)?;
|
||||||
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) =
|
let (data_type, nullable) = projection_metadata(expr, &input_schema)?;
|
||||||
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,
|
||||||
});
|
});
|
||||||
fields.push(Field::new(output_name, data_type, nullable));
|
fields.push(Field::new(output_name, data_type, nullable));
|
||||||
}
|
}
|
||||||
SelectItem::Wildcard => return Err(PlannerError::MixedWildcardProjection),
|
SelectItem::Wildcard => unreachable!("wildcard projections are handled earlier"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plan = LogicalPlan::Project {
|
let plan = LogicalPlan::Project {
|
||||||
input: Box::new(plan),
|
input: Box::new(plan),
|
||||||
expressions,
|
expressions,
|
||||||
schema: Schema::new(fields),
|
schema: Schema::new(fields),
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
let output_schema = plan.output_schema().clone();
|
let output_schema = plan.output_schema().clone();
|
||||||
plan = maybe_apply_sort(plan, output_schema, &select.order_by, &select.from)?;
|
maybe_apply_sort(plan, output_schema, &select.order_by)
|
||||||
|
|
||||||
if let Some(count) = select.limit {
|
|
||||||
plan = LogicalPlan::Limit {
|
|
||||||
input: Box::new(plan),
|
|
||||||
count,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(plan)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_wildcard_projection(items: &[SelectItem]) -> bool {
|
fn is_wildcard_projection(items: &[SelectItem]) -> bool {
|
||||||
@ -141,8 +125,7 @@ fn plan_from_tables(
|
|||||||
return Err(PlannerError::DuplicateSourceName(first_name));
|
return Err(PlannerError::DuplicateSourceName(first_name));
|
||||||
}
|
}
|
||||||
|
|
||||||
let first_schema =
|
let first_schema = input_schema_for_table(first, catalog, tables.len() > 1)?;
|
||||||
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(),
|
||||||
@ -155,8 +138,7 @@ fn plan_from_tables(
|
|||||||
return Err(PlannerError::DuplicateSourceName(qualified_name));
|
return Err(PlannerError::DuplicateSourceName(qualified_name));
|
||||||
}
|
}
|
||||||
|
|
||||||
let right_schema =
|
let right_schema = input_schema_for_table(table, catalog, tables.len() > 1)?;
|
||||||
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(),
|
||||||
@ -173,33 +155,23 @@ fn plan_from_tables(
|
|||||||
Ok((plan, combined_schema))
|
Ok((plan, combined_schema))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn plan_expr(
|
fn plan_expr(expr: &Expr, schema: &Schema) -> Result<LogicalExpr, PlannerError> {
|
||||||
expr: &Expr,
|
|
||||||
schema: &Schema,
|
|
||||||
tables: &[TableRef],
|
|
||||||
) -> Result<LogicalExpr, PlannerError> {
|
|
||||||
match expr {
|
match expr {
|
||||||
Expr::Identifier(name) => {
|
Expr::Identifier(name) => {
|
||||||
let resolved = resolve_column_name(name, schema, tables)?;
|
if schema.index_of(name).is_none() {
|
||||||
Ok(LogicalExpr::Column(resolved))
|
return Err(PlannerError::UnknownColumn(name.clone()));
|
||||||
|
}
|
||||||
|
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, tables)?),
|
Box::new(plan_expr(left, schema)?),
|
||||||
Box::new(plan_expr(right, schema, tables)?),
|
Box::new(plan_expr(right, schema)?),
|
||||||
)),
|
|
||||||
BinaryOp::Ne => Ok(LogicalExpr::Ne(
|
|
||||||
Box::new(plan_expr(left, schema, tables)?),
|
|
||||||
Box::new(plan_expr(right, schema, tables)?),
|
|
||||||
)),
|
)),
|
||||||
BinaryOp::And => Ok(LogicalExpr::And(
|
BinaryOp::And => Ok(LogicalExpr::And(
|
||||||
Box::new(plan_expr(left, schema, tables)?),
|
Box::new(plan_expr(left, schema)?),
|
||||||
Box::new(plan_expr(right, schema, tables)?),
|
Box::new(plan_expr(right, schema)?),
|
||||||
)),
|
|
||||||
BinaryOp::Or => Ok(LogicalExpr::Or(
|
|
||||||
Box::new(plan_expr(left, schema, tables)?),
|
|
||||||
Box::new(plan_expr(right, schema, tables)?),
|
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -209,7 +181,6 @@ 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);
|
||||||
@ -221,7 +192,9 @@ fn maybe_apply_sort(
|
|||||||
Expr::Identifier(name) => name.clone(),
|
Expr::Identifier(name) => name.clone(),
|
||||||
_ => return Err(PlannerError::UnsupportedOrderBy),
|
_ => return Err(PlannerError::UnsupportedOrderBy),
|
||||||
};
|
};
|
||||||
let column = resolve_column_name(&column, &schema, tables)?;
|
if schema.index_of(&column).is_none() {
|
||||||
|
return Err(PlannerError::UnknownColumn(column));
|
||||||
|
}
|
||||||
keys.push(SortKey {
|
keys.push(SortKey {
|
||||||
column,
|
column,
|
||||||
direction: match item.direction {
|
direction: match item.direction {
|
||||||
@ -241,53 +214,25 @@ fn maybe_apply_sort(
|
|||||||
fn plan_literal(literal: &Literal) -> Value {
|
fn plan_literal(literal: &Literal) -> Value {
|
||||||
match literal {
|
match literal {
|
||||||
Literal::String(value) => Value::text(value.clone()),
|
Literal::String(value) => Value::text(value.clone()),
|
||||||
Literal::Integer(n) => Value::Integer(*n),
|
|
||||||
Literal::Null => Value::Null,
|
Literal::Null => Value::Null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn projection_metadata(
|
fn projection_metadata(expr: &Expr, schema: &Schema) -> Result<(DataType, bool), PlannerError> {
|
||||||
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(&resolved)
|
.index_of(name)
|
||||||
.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()))
|
||||||
}
|
}
|
||||||
Expr::Literal(Literal::String(_)) => Ok((DataType::Text, false)),
|
Expr::Literal(Literal::String(_)) => Ok((DataType::Text, false)),
|
||||||
Expr::Literal(Literal::Integer(_)) => Ok((DataType::Integer, false)),
|
|
||||||
Expr::Literal(Literal::Null) => Ok((DataType::Text, true)),
|
Expr::Literal(Literal::Null) => Ok((DataType::Text, true)),
|
||||||
Expr::Binary { .. } => Ok((DataType::Boolean, true)),
|
Expr::Binary { .. } => Ok((DataType::Boolean, true)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(),
|
||||||
@ -321,10 +266,6 @@ 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())
|
||||||
}
|
}
|
||||||
@ -340,7 +281,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::catalog::PredicateCatalog;
|
use crate::catalog::PredicateCatalog;
|
||||||
use crate::chase::{Atom, Instance, Term};
|
use crate::chase::{Atom, Instance, Term};
|
||||||
use crate::sql::parser::{ParseError, parse_select};
|
use crate::sql::parser::parse_select;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn plans_projection_and_filter() {
|
fn plans_projection_and_filter() {
|
||||||
@ -445,50 +386,6 @@ 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]
|
|
||||||
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(
|
||||||
@ -541,38 +438,4 @@ mod tests {
|
|||||||
other => panic!("unexpected plan: {:?}", other),
|
other => panic!("unexpected plan: {:?}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn rejects_mixed_wildcard_projection() {
|
|
||||||
let instance: Instance = vec![Atom::new(
|
|
||||||
"Parent",
|
|
||||||
vec![Term::constant("alice"), Term::constant("bob")],
|
|
||||||
)]
|
|
||||||
.into_iter()
|
|
||||||
.collect();
|
|
||||||
let catalog = PredicateCatalog::from_instance(&instance).unwrap();
|
|
||||||
let select = parse_select("SELECT *, c0 FROM Parent").unwrap_err();
|
|
||||||
assert_eq!(select, ParseError::MixedWildcardProjection);
|
|
||||||
let malformed = Select {
|
|
||||||
projection: vec![
|
|
||||||
SelectItem::Wildcard,
|
|
||||||
SelectItem::Expr {
|
|
||||||
expr: Expr::Identifier("c0".to_string()),
|
|
||||||
alias: None,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
from: vec![TableRef {
|
|
||||||
name: "Parent".to_string(),
|
|
||||||
alias: None,
|
|
||||||
}],
|
|
||||||
selection: None,
|
|
||||||
order_by: Vec::new(),
|
|
||||||
limit: None,
|
|
||||||
};
|
|
||||||
let error = plan_select(&malformed, &catalog).unwrap_err();
|
|
||||||
assert_eq!(
|
|
||||||
error.to_string(),
|
|
||||||
"wildcard projections cannot be combined with other items"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,6 @@ use std::fmt;
|
|||||||
pub enum DataType {
|
pub enum DataType {
|
||||||
/// UTF-8 text values.
|
/// UTF-8 text values.
|
||||||
Text,
|
Text,
|
||||||
/// 64-bit signed integer values.
|
|
||||||
Integer,
|
|
||||||
/// Boolean values.
|
/// Boolean values.
|
||||||
Boolean,
|
Boolean,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,6 @@ use std::fmt;
|
|||||||
pub enum Value {
|
pub enum Value {
|
||||||
/// Textual data.
|
/// Textual data.
|
||||||
Text(String),
|
Text(String),
|
||||||
/// Integer data.
|
|
||||||
Integer(i64),
|
|
||||||
/// Boolean data.
|
/// Boolean data.
|
||||||
Boolean(bool),
|
Boolean(bool),
|
||||||
/// SQL-style null.
|
/// SQL-style null.
|
||||||
@ -31,9 +29,8 @@ impl Value {
|
|||||||
match (self, other) {
|
match (self, other) {
|
||||||
(Self::Null, _) | (_, Self::Null) => None,
|
(Self::Null, _) | (_, Self::Null) => None,
|
||||||
(Self::Text(left), Self::Text(right)) => Some(left == right),
|
(Self::Text(left), Self::Text(right)) => Some(left == right),
|
||||||
(Self::Integer(left), Self::Integer(right)) => Some(left == right),
|
|
||||||
(Self::Boolean(left), Self::Boolean(right)) => Some(left == right),
|
(Self::Boolean(left), Self::Boolean(right)) => Some(left == right),
|
||||||
_ => Some(false),
|
(Self::Text(_), Self::Boolean(_)) | (Self::Boolean(_), Self::Text(_)) => Some(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,7 +39,6 @@ impl fmt::Display for Value {
|
|||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Text(value) => write!(f, "{}", value),
|
Self::Text(value) => write!(f, "{}", value),
|
||||||
Self::Integer(value) => write!(f, "{}", value),
|
|
||||||
Self::Boolean(value) => write!(f, "{}", value),
|
Self::Boolean(value) => write!(f, "{}", value),
|
||||||
Self::Null => write!(f, "NULL"),
|
Self::Null => write!(f, "NULL"),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
/// A parsed `SELECT-FROM-WHERE-ORDER BY-LIMIT` statement in the current SQL subset.
|
/// A parsed `SELECT-FROM-WHERE-ORDER BY` statement in the current SQL subset.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Select {
|
pub struct Select {
|
||||||
/// Output expressions requested by the query.
|
/// Output expressions requested by the query.
|
||||||
@ -9,8 +9,6 @@ pub struct Select {
|
|||||||
pub selection: Option<Expr>,
|
pub selection: Option<Expr>,
|
||||||
/// Optional output ordering.
|
/// Optional output ordering.
|
||||||
pub order_by: Vec<OrderByItem>,
|
pub order_by: Vec<OrderByItem>,
|
||||||
/// Optional row limit.
|
|
||||||
pub limit: Option<usize>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One source entry in a `FROM` list.
|
/// One source entry in a `FROM` list.
|
||||||
@ -60,8 +58,6 @@ pub enum Expr {
|
|||||||
pub enum Literal {
|
pub enum Literal {
|
||||||
/// A string literal.
|
/// A string literal.
|
||||||
String(String),
|
String(String),
|
||||||
/// An integer literal.
|
|
||||||
Integer(i64),
|
|
||||||
/// The `NULL` literal.
|
/// The `NULL` literal.
|
||||||
Null,
|
Null,
|
||||||
}
|
}
|
||||||
@ -71,12 +67,8 @@ pub enum Literal {
|
|||||||
pub enum BinaryOp {
|
pub enum BinaryOp {
|
||||||
/// Equality.
|
/// Equality.
|
||||||
Eq,
|
Eq,
|
||||||
/// Inequality.
|
|
||||||
Ne,
|
|
||||||
/// Boolean conjunction.
|
/// Boolean conjunction.
|
||||||
And,
|
And,
|
||||||
/// Boolean disjunction.
|
|
||||||
Or,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sort direction for `ORDER BY`.
|
/// Sort direction for `ORDER BY`.
|
||||||
|
|||||||
@ -12,7 +12,6 @@ pub enum ParseError {
|
|||||||
ExpectedToken(&'static str),
|
ExpectedToken(&'static str),
|
||||||
ExpectedIdentifier,
|
ExpectedIdentifier,
|
||||||
UnexpectedToken(String),
|
UnexpectedToken(String),
|
||||||
MixedWildcardProjection,
|
|
||||||
UnterminatedString,
|
UnterminatedString,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,12 +22,6 @@ impl fmt::Display for ParseError {
|
|||||||
Self::ExpectedToken(token) => write!(f, "expected `{}`", token),
|
Self::ExpectedToken(token) => write!(f, "expected `{}`", token),
|
||||||
Self::ExpectedIdentifier => write!(f, "expected identifier"),
|
Self::ExpectedIdentifier => write!(f, "expected identifier"),
|
||||||
Self::UnexpectedToken(token) => write!(f, "unexpected token `{}`", token),
|
Self::UnexpectedToken(token) => write!(f, "unexpected token `{}`", token),
|
||||||
Self::MixedWildcardProjection => {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"wildcard projections cannot be combined with other items"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Self::UnterminatedString => write!(f, "unterminated string literal"),
|
Self::UnterminatedString => write!(f, "unterminated string literal"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,20 +36,16 @@ enum Token {
|
|||||||
Where,
|
Where,
|
||||||
As,
|
As,
|
||||||
And,
|
And,
|
||||||
Or,
|
|
||||||
Order,
|
Order,
|
||||||
By,
|
By,
|
||||||
Asc,
|
Asc,
|
||||||
Desc,
|
Desc,
|
||||||
Null,
|
Null,
|
||||||
Limit,
|
|
||||||
Identifier(String),
|
Identifier(String),
|
||||||
String(String),
|
String(String),
|
||||||
Integer(usize),
|
|
||||||
Star,
|
Star,
|
||||||
Comma,
|
Comma,
|
||||||
Eq,
|
Eq,
|
||||||
Ne,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a `SELECT-FROM-WHERE-ORDER BY` query in the current SQL subset.
|
/// Parse a `SELECT-FROM-WHERE-ORDER BY` query in the current SQL subset.
|
||||||
@ -95,13 +84,6 @@ impl Parser {
|
|||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let limit = if self.peek() == Some(&Token::Limit) {
|
|
||||||
self.index += 1;
|
|
||||||
Some(self.expect_integer()?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(token) = self.peek() {
|
if let Some(token) = self.peek() {
|
||||||
return Err(ParseError::UnexpectedToken(render_token(token)));
|
return Err(ParseError::UnexpectedToken(render_token(token)));
|
||||||
}
|
}
|
||||||
@ -111,7 +93,6 @@ impl Parser {
|
|||||||
from,
|
from,
|
||||||
selection,
|
selection,
|
||||||
order_by,
|
order_by,
|
||||||
limit,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,14 +125,6 @@ impl Parser {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if items.len() > 1
|
|
||||||
&& items
|
|
||||||
.iter()
|
|
||||||
.any(|item| matches!(item, SelectItem::Wildcard))
|
|
||||||
{
|
|
||||||
return Err(ParseError::MixedWildcardProjection);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(items)
|
Ok(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,22 +151,6 @@ impl Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_expr(&mut self) -> Result<Expr, ParseError> {
|
fn parse_expr(&mut self) -> Result<Expr, ParseError> {
|
||||||
let mut expr = self.parse_and()?;
|
|
||||||
|
|
||||||
while self.peek() == Some(&Token::Or) {
|
|
||||||
self.index += 1;
|
|
||||||
let right = self.parse_and()?;
|
|
||||||
expr = Expr::Binary {
|
|
||||||
left: Box::new(expr),
|
|
||||||
op: BinaryOp::Or,
|
|
||||||
right: Box::new(right),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_and(&mut self) -> Result<Expr, ParseError> {
|
|
||||||
let mut expr = self.parse_equality()?;
|
let mut expr = self.parse_equality()?;
|
||||||
|
|
||||||
while self.peek() == Some(&Token::And) {
|
while self.peek() == Some(&Token::And) {
|
||||||
@ -248,14 +205,6 @@ impl Parser {
|
|||||||
right: Box::new(right),
|
right: Box::new(right),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Token::Ne => {
|
|
||||||
let right = self.parse_operand()?;
|
|
||||||
Ok(Expr::Binary {
|
|
||||||
left: Box::new(left),
|
|
||||||
op: BinaryOp::Ne,
|
|
||||||
right: Box::new(right),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
other => Err(ParseError::UnexpectedToken(render_token(&other))),
|
other => Err(ParseError::UnexpectedToken(render_token(&other))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -264,7 +213,6 @@ impl Parser {
|
|||||||
match self.next().ok_or(ParseError::UnexpectedEnd)? {
|
match self.next().ok_or(ParseError::UnexpectedEnd)? {
|
||||||
Token::Identifier(name) => Ok(Expr::Identifier(name)),
|
Token::Identifier(name) => Ok(Expr::Identifier(name)),
|
||||||
Token::String(value) => Ok(Expr::Literal(Literal::String(value))),
|
Token::String(value) => Ok(Expr::Literal(Literal::String(value))),
|
||||||
Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n as i64))),
|
|
||||||
Token::Null => Ok(Expr::Literal(Literal::Null)),
|
Token::Null => Ok(Expr::Literal(Literal::Null)),
|
||||||
other => Err(ParseError::UnexpectedToken(render_token(&other))),
|
other => Err(ParseError::UnexpectedToken(render_token(&other))),
|
||||||
}
|
}
|
||||||
@ -286,13 +234,6 @@ impl Parser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expect_integer(&mut self) -> Result<usize, ParseError> {
|
|
||||||
match self.next().ok_or(ParseError::UnexpectedEnd)? {
|
|
||||||
Token::Integer(n) => Ok(n),
|
|
||||||
other => Err(ParseError::UnexpectedToken(render_token(&other))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn peek(&self) -> Option<&Token> {
|
fn peek(&self) -> Option<&Token> {
|
||||||
self.tokens.get(self.index)
|
self.tokens.get(self.index)
|
||||||
}
|
}
|
||||||
@ -325,33 +266,11 @@ fn tokenize(input: &str) -> Result<Vec<Token>, ParseError> {
|
|||||||
chars.next();
|
chars.next();
|
||||||
tokens.push(Token::Comma);
|
tokens.push(Token::Comma);
|
||||||
}
|
}
|
||||||
'!' => {
|
|
||||||
chars.next();
|
|
||||||
if chars.peek() == Some(&'=') {
|
|
||||||
chars.next();
|
|
||||||
tokens.push(Token::Ne);
|
|
||||||
} else {
|
|
||||||
return Err(ParseError::UnexpectedToken("!".to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'<' => {
|
|
||||||
chars.next();
|
|
||||||
if chars.peek() == Some(&'>') {
|
|
||||||
chars.next();
|
|
||||||
tokens.push(Token::Ne);
|
|
||||||
} else {
|
|
||||||
return Err(ParseError::UnexpectedToken("<".to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'=' => {
|
'=' => {
|
||||||
chars.next();
|
chars.next();
|
||||||
tokens.push(Token::Eq);
|
tokens.push(Token::Eq);
|
||||||
}
|
}
|
||||||
'\'' => tokens.push(Token::String(parse_string(&mut chars)?)),
|
'\'' => tokens.push(Token::String(parse_string(&mut chars)?)),
|
||||||
ch if ch.is_ascii_digit() => {
|
|
||||||
let number = parse_integer(&mut chars);
|
|
||||||
tokens.push(Token::Integer(number));
|
|
||||||
}
|
|
||||||
ch if is_identifier_start(ch) => {
|
ch if is_identifier_start(ch) => {
|
||||||
let ident = parse_identifier(&mut chars);
|
let ident = parse_identifier(&mut chars);
|
||||||
let token = match ident.to_ascii_uppercase().as_str() {
|
let token = match ident.to_ascii_uppercase().as_str() {
|
||||||
@ -360,13 +279,11 @@ fn tokenize(input: &str) -> Result<Vec<Token>, ParseError> {
|
|||||||
"WHERE" => Token::Where,
|
"WHERE" => Token::Where,
|
||||||
"AS" => Token::As,
|
"AS" => Token::As,
|
||||||
"AND" => Token::And,
|
"AND" => Token::And,
|
||||||
"OR" => Token::Or,
|
|
||||||
"ORDER" => Token::Order,
|
"ORDER" => Token::Order,
|
||||||
"BY" => Token::By,
|
"BY" => Token::By,
|
||||||
"ASC" => Token::Asc,
|
"ASC" => Token::Asc,
|
||||||
"DESC" => Token::Desc,
|
"DESC" => Token::Desc,
|
||||||
"NULL" => Token::Null,
|
"NULL" => Token::Null,
|
||||||
"LIMIT" => Token::Limit,
|
|
||||||
_ => Token::Identifier(ident),
|
_ => Token::Identifier(ident),
|
||||||
};
|
};
|
||||||
tokens.push(token);
|
tokens.push(token);
|
||||||
@ -419,24 +336,8 @@ where
|
|||||||
ident
|
ident
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_integer<I>(chars: &mut std::iter::Peekable<I>) -> usize
|
|
||||||
where
|
|
||||||
I: Iterator<Item = char>,
|
|
||||||
{
|
|
||||||
let mut value: usize = 0;
|
|
||||||
while let Some(ch) = chars.peek().copied() {
|
|
||||||
if ch.is_ascii_digit() {
|
|
||||||
value = value * 10 + (ch as usize - '0' as usize);
|
|
||||||
chars.next();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_identifier_start(ch: char) -> bool {
|
fn is_identifier_start(ch: char) -> bool {
|
||||||
ch.is_ascii_alphabetic() || ch == '_'
|
ch.is_ascii_alphanumeric() || ch == '_'
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_identifier_part(ch: char) -> bool {
|
fn is_identifier_part(ch: char) -> bool {
|
||||||
@ -450,20 +351,16 @@ fn render_token(token: &Token) -> String {
|
|||||||
Token::Where => "WHERE".to_string(),
|
Token::Where => "WHERE".to_string(),
|
||||||
Token::As => "AS".to_string(),
|
Token::As => "AS".to_string(),
|
||||||
Token::And => "AND".to_string(),
|
Token::And => "AND".to_string(),
|
||||||
Token::Or => "OR".to_string(),
|
|
||||||
Token::Order => "ORDER".to_string(),
|
Token::Order => "ORDER".to_string(),
|
||||||
Token::By => "BY".to_string(),
|
Token::By => "BY".to_string(),
|
||||||
Token::Asc => "ASC".to_string(),
|
Token::Asc => "ASC".to_string(),
|
||||||
Token::Desc => "DESC".to_string(),
|
Token::Desc => "DESC".to_string(),
|
||||||
Token::Null => "NULL".to_string(),
|
Token::Null => "NULL".to_string(),
|
||||||
Token::Limit => "LIMIT".to_string(),
|
|
||||||
Token::Identifier(name) => name.clone(),
|
Token::Identifier(name) => name.clone(),
|
||||||
Token::Integer(n) => n.to_string(),
|
|
||||||
Token::String(value) => format!("'{}'", value),
|
Token::String(value) => format!("'{}'", value),
|
||||||
Token::Star => "*".to_string(),
|
Token::Star => "*".to_string(),
|
||||||
Token::Comma => ",".to_string(),
|
Token::Comma => ",".to_string(),
|
||||||
Token::Eq => "=".to_string(),
|
Token::Eq => "=".to_string(),
|
||||||
Token::Ne => "!=".to_string(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -618,121 +515,4 @@ mod tests {
|
|||||||
assert_eq!(select.from.len(), 1);
|
assert_eq!(select.from.len(), 1);
|
||||||
assert_eq!(select.from[0].name, "Employee-Records:2025");
|
assert_eq!(select.from[0].name, "Employee-Records:2025");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn rejects_mixed_wildcard_projection() {
|
|
||||||
let error = parse_select("SELECT *, c0 FROM Parent").unwrap_err();
|
|
||||||
assert_eq!(error, ParseError::MixedWildcardProjection);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parses_not_equal_with_bang_eq() {
|
|
||||||
let select = parse_select("SELECT c0 FROM Parent WHERE c1 != 'bob'").unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
select.selection,
|
|
||||||
Some(Expr::Binary {
|
|
||||||
left: Box::new(Expr::Identifier("c1".to_string())),
|
|
||||||
op: BinaryOp::Ne,
|
|
||||||
right: Box::new(Expr::Literal(Literal::String("bob".to_string()))),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parses_not_equal_with_diamond() {
|
|
||||||
let select = parse_select("SELECT c0 FROM Parent WHERE c1 <> 'bob'").unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
select.selection,
|
|
||||||
Some(Expr::Binary {
|
|
||||||
left: Box::new(Expr::Identifier("c1".to_string())),
|
|
||||||
op: BinaryOp::Ne,
|
|
||||||
right: Box::new(Expr::Literal(Literal::String("bob".to_string()))),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parses_or_expression() {
|
|
||||||
let select =
|
|
||||||
parse_select("SELECT c0 FROM Parent WHERE c0 = 'alice' OR c0 = 'bob'").unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
select.selection,
|
|
||||||
Some(Expr::Binary {
|
|
||||||
left: Box::new(Expr::Binary {
|
|
||||||
left: Box::new(Expr::Identifier("c0".to_string())),
|
|
||||||
op: BinaryOp::Eq,
|
|
||||||
right: Box::new(Expr::Literal(Literal::String("alice".to_string()))),
|
|
||||||
}),
|
|
||||||
op: BinaryOp::Or,
|
|
||||||
right: Box::new(Expr::Binary {
|
|
||||||
left: Box::new(Expr::Identifier("c0".to_string())),
|
|
||||||
op: BinaryOp::Eq,
|
|
||||||
right: Box::new(Expr::Literal(Literal::String("bob".to_string()))),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parses_integer_literal_in_expression() {
|
|
||||||
let select = parse_select("SELECT c0 FROM Parent WHERE c0 = 42").unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
select.selection,
|
|
||||||
Some(Expr::Binary {
|
|
||||||
left: Box::new(Expr::Identifier("c0".to_string())),
|
|
||||||
op: BinaryOp::Eq,
|
|
||||||
right: Box::new(Expr::Literal(Literal::Integer(42))),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parses_limit_clause() {
|
|
||||||
let select = parse_select("SELECT c0 FROM Parent LIMIT 5").unwrap();
|
|
||||||
assert_eq!(select.limit, Some(5));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parses_order_by_with_limit() {
|
|
||||||
let select = parse_select("SELECT c0 FROM Parent ORDER BY c0 DESC LIMIT 1").unwrap();
|
|
||||||
assert_eq!(select.order_by.len(), 1);
|
|
||||||
assert_eq!(select.limit, Some(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parses_or_with_and_precedence() {
|
|
||||||
// AND binds tighter than OR: a = '1' OR b = '2' AND c = '3'
|
|
||||||
// should parse as: a = '1' OR (b = '2' AND c = '3')
|
|
||||||
let select =
|
|
||||||
parse_select("SELECT c0 FROM Parent WHERE c0 = '1' OR c1 = '2' AND c0 = '3'").unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
select.selection,
|
|
||||||
Some(Expr::Binary {
|
|
||||||
left: Box::new(Expr::Binary {
|
|
||||||
left: Box::new(Expr::Identifier("c0".to_string())),
|
|
||||||
op: BinaryOp::Eq,
|
|
||||||
right: Box::new(Expr::Literal(Literal::String("1".to_string()))),
|
|
||||||
}),
|
|
||||||
op: BinaryOp::Or,
|
|
||||||
right: Box::new(Expr::Binary {
|
|
||||||
left: Box::new(Expr::Binary {
|
|
||||||
left: Box::new(Expr::Identifier("c1".to_string())),
|
|
||||||
op: BinaryOp::Eq,
|
|
||||||
right: Box::new(Expr::Literal(Literal::String("2".to_string()))),
|
|
||||||
}),
|
|
||||||
op: BinaryOp::And,
|
|
||||||
right: Box::new(Expr::Binary {
|
|
||||||
left: Box::new(Expr::Identifier("c0".to_string())),
|
|
||||||
op: BinaryOp::Eq,
|
|
||||||
right: Box::new(Expr::Literal(Literal::String("3".to_string()))),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -101,25 +101,6 @@ 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![
|
||||||
@ -250,82 +231,3 @@ fn select_order_by_desc_sorts_rows() {
|
|||||||
assert_eq!(format!("{}", result.rows()[0].values()[0]), "bob");
|
assert_eq!(format!("{}", result.rows()[0].values()[0]), "bob");
|
||||||
assert_eq!(format!("{}", result.rows()[1].values()[0]), "alice");
|
assert_eq!(format!("{}", result.rows()[1].values()[0]), "alice");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn select_integer_literal_in_projection() {
|
|
||||||
let instance = parent_instance();
|
|
||||||
let catalog = PredicateCatalog::from_instance(&instance).unwrap();
|
|
||||||
let select = parse_select("SELECT c0, 42 AS answer FROM Parent").unwrap();
|
|
||||||
|
|
||||||
let plan = plan_select(&select, &catalog).unwrap();
|
|
||||||
let result = execute(&plan, &instance).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(result.schema().len(), 2);
|
|
||||||
assert_eq!(result.schema().fields()[1].name(), "answer");
|
|
||||||
assert_eq!(
|
|
||||||
result.schema().fields()[1].data_type(),
|
|
||||||
&query_engine::relational::DataType::Integer
|
|
||||||
);
|
|
||||||
assert_eq!(result.rows().len(), 2);
|
|
||||||
assert_eq!(format!("{}", result.rows()[0].values()[1]), "42");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn select_limit_restricts_row_count() {
|
|
||||||
let instance = parent_instance();
|
|
||||||
let catalog = PredicateCatalog::from_instance(&instance).unwrap();
|
|
||||||
let select = parse_select("SELECT c0 FROM Parent ORDER BY c0 ASC LIMIT 1").unwrap();
|
|
||||||
|
|
||||||
let plan = plan_select(&select, &catalog).unwrap();
|
|
||||||
let result = execute(&plan, &instance).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(result.rows().len(), 1);
|
|
||||||
assert_eq!(format!("{}", result.rows()[0].values()[0]), "alice");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn select_where_or_matches_either_condition() {
|
|
||||||
let instance: Instance = vec![
|
|
||||||
Atom::new(
|
|
||||||
"Parent",
|
|
||||||
vec![Term::constant("alice"), Term::constant("bob")],
|
|
||||||
),
|
|
||||||
Atom::new(
|
|
||||||
"Parent",
|
|
||||||
vec![Term::constant("bob"), Term::constant("carol")],
|
|
||||||
),
|
|
||||||
Atom::new(
|
|
||||||
"Parent",
|
|
||||||
vec![Term::constant("carol"), Term::constant("dave")],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.collect();
|
|
||||||
let catalog = PredicateCatalog::from_instance(&instance).unwrap();
|
|
||||||
let select = parse_select("SELECT c0 FROM Parent WHERE c1 = 'bob' OR c1 = 'dave'").unwrap();
|
|
||||||
|
|
||||||
let plan = plan_select(&select, &catalog).unwrap();
|
|
||||||
let result = execute(&plan, &instance).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(result.rows().len(), 2);
|
|
||||||
let mut values = result
|
|
||||||
.rows()
|
|
||||||
.iter()
|
|
||||||
.map(|row| format!("{}", row.values()[0]))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
values.sort();
|
|
||||||
assert_eq!(values, vec!["alice".to_string(), "carol".to_string()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn select_where_not_equal_excludes_matching_rows() {
|
|
||||||
let instance = parent_instance();
|
|
||||||
let catalog = PredicateCatalog::from_instance(&instance).unwrap();
|
|
||||||
let select = parse_select("SELECT c0 FROM Parent WHERE c1 != 'bob'").unwrap();
|
|
||||||
|
|
||||||
let plan = plan_select(&select, &catalog).unwrap();
|
|
||||||
let result = execute(&plan, &instance).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(result.rows().len(), 1);
|
|
||||||
assert_eq!(format!("{}", result.rows()[0].values()[0]), "bob");
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user