Compare commits
No commits in common. "f0d22976c74611d35b270ba90bbea0892c3dc758" and "52cb492bce4286bb25878545183d1a7cbf96f079" have entirely different histories.
f0d22976c7
...
52cb492bce
@ -59,7 +59,7 @@ Quick examples:
|
||||
- `src/catalog/`: predicate-to-table schema inference and catalog access.
|
||||
- `src/sql/`: narrow SQL AST and parser support.
|
||||
- `src/planner/`: logical plan structures and SQL-to-plan translation.
|
||||
- `src/execution/`: execution of the current logical plan subset, including the `DataSource` trait and the `TableStore` in-memory source.
|
||||
- `src/execution/`: execution of the current logical plan subset.
|
||||
- `examples/scripts/`: runnable script examples for supported workflows.
|
||||
- `tests/`: integration, regression, and property-based tests.
|
||||
|
||||
@ -75,7 +75,6 @@ Quick examples:
|
||||
- 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.
|
||||
- The executor operates on the `DataSource` trait, not on `Instance` directly. `Instance` and `TableStore` are the two built-in implementations.
|
||||
- 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.
|
||||
- Public docs and interfaces should reflect the implemented state of the repository accurately.
|
||||
|
||||
12
README.md
12
README.md
@ -26,22 +26,12 @@ The repository is currently organized around a few clear subsystems:
|
||||
- `src/catalog/`: predicate-backed table metadata
|
||||
- `src/sql/`: minimal SQL AST and parser
|
||||
- `src/planner/`: logical plan structures and SQL-to-plan translation
|
||||
- `src/execution/`: execution for the current logical-plan subset, `DataSource` trait, and `TableStore`
|
||||
- `src/execution/`: execution for the current logical-plan subset
|
||||
|
||||
Today, the chase subsystem is still the most mature part of the codebase. The
|
||||
relational and SQL modules are present to create clean extension points for a
|
||||
broader query-engine architecture.
|
||||
|
||||
The executor operates on the `DataSource` trait rather than on the chase
|
||||
`Instance` directly. This allows non-chase data sources to plug into the SQL
|
||||
pipeline. The crate ships two implementations: `Instance` (chase-backed) and
|
||||
`TableStore` (in-memory rows). Implementing `DataSource` for a new backend
|
||||
requires a single method:
|
||||
|
||||
```rust
|
||||
fn scan(&self, table: &str, schema: &Schema) -> Result<ResultSet, ExecutionError>;
|
||||
```
|
||||
|
||||
### Intended Direction
|
||||
|
||||
The medium-term direction is to evolve this project into a more general
|
||||
|
||||
@ -48,9 +48,9 @@ This document tracks the current state and next steps for the repository.
|
||||
|
||||
- [x] Introduce a dedicated logical representation module
|
||||
- [x] Define clear front-end, planning, and execution boundaries
|
||||
- [x] Add engine-level abstractions that are not chase-specific
|
||||
- [ ] Add engine-level abstractions that are not chase-specific
|
||||
- [x] Establish common schema and typed-value representations
|
||||
- [x] Design a source boundary for future scans and pushdown
|
||||
- [ ] Design a source boundary for future scans and pushdown
|
||||
|
||||
### Front End and Planning
|
||||
|
||||
|
||||
@ -1,10 +1,4 @@
|
||||
//! Execution support for the current SQL slice.
|
||||
//!
|
||||
//! The executor evaluates a [`LogicalPlan`] against a [`DataSource`] that
|
||||
//! provides table scans. The built-in [`Instance`](crate::chase::Instance)
|
||||
//! adapter and the [`TableStore`] are the two provided implementations.
|
||||
|
||||
pub mod table_store;
|
||||
//! Minimal execution support for the first SQL slice.
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::error::Error;
|
||||
@ -12,9 +6,7 @@ use std::fmt;
|
||||
|
||||
use crate::chase::{Instance, Term};
|
||||
use crate::planner::logical::{LogicalExpr, LogicalPlan, SortDirection, SortKey};
|
||||
use crate::relational::{ResultSet, Row, Schema, Value};
|
||||
|
||||
pub use table_store::TableStore;
|
||||
use crate::relational::{ResultSet, Row, Value};
|
||||
|
||||
/// Errors returned by the current logical-plan executor.
|
||||
#[derive(Debug)]
|
||||
@ -38,21 +30,12 @@ impl fmt::Display for ExecutionError {
|
||||
|
||||
impl Error for ExecutionError {}
|
||||
|
||||
/// A source of relational data for the executor.
|
||||
///
|
||||
/// Implementations provide table scans that return rows conforming to a given
|
||||
/// schema. The executor calls [`scan`](DataSource::scan) for each
|
||||
/// [`LogicalPlan::Scan`] node; all other operators work on the resulting
|
||||
/// [`ResultSet`] values.
|
||||
pub trait DataSource {
|
||||
/// Scan all rows for the named table, conforming to the provided schema.
|
||||
fn scan(&self, table: &str, schema: &Schema) -> Result<ResultSet, ExecutionError>;
|
||||
}
|
||||
|
||||
impl DataSource for Instance {
|
||||
fn scan(&self, table: &str, schema: &Schema) -> Result<ResultSet, ExecutionError> {
|
||||
/// Execute the current logical-plan subset against an instance-backed source.
|
||||
pub fn execute(plan: &LogicalPlan, instance: &Instance) -> Result<ResultSet, ExecutionError> {
|
||||
match plan {
|
||||
LogicalPlan::Scan { table, schema } => {
|
||||
let mut rows = Vec::new();
|
||||
for fact in self.facts_for_predicate(table) {
|
||||
for fact in instance.facts_for_predicate(table) {
|
||||
let values = fact
|
||||
.terms
|
||||
.iter()
|
||||
@ -62,19 +45,13 @@ impl DataSource for Instance {
|
||||
}
|
||||
Ok(ResultSet::new(schema.clone(), rows))
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a logical plan against the provided data source.
|
||||
pub fn execute(plan: &LogicalPlan, source: &dyn DataSource) -> Result<ResultSet, ExecutionError> {
|
||||
match plan {
|
||||
LogicalPlan::Scan { table, schema } => source.scan(table, schema),
|
||||
LogicalPlan::CrossJoin {
|
||||
left,
|
||||
right,
|
||||
schema,
|
||||
} => {
|
||||
let left_result = execute(left, source)?;
|
||||
let right_result = execute(right, source)?;
|
||||
let left_result = execute(left, instance)?;
|
||||
let right_result = execute(right, instance)?;
|
||||
let mut rows = Vec::new();
|
||||
|
||||
for left_row in left_result.rows() {
|
||||
@ -88,7 +65,7 @@ pub fn execute(plan: &LogicalPlan, source: &dyn DataSource) -> Result<ResultSet,
|
||||
Ok(ResultSet::new(schema.clone(), rows))
|
||||
}
|
||||
LogicalPlan::Filter { input, predicate } => {
|
||||
let result = execute(input, source)?;
|
||||
let result = execute(input, instance)?;
|
||||
let filtered_rows = result
|
||||
.rows()
|
||||
.iter()
|
||||
@ -102,7 +79,7 @@ pub fn execute(plan: &LogicalPlan, source: &dyn DataSource) -> Result<ResultSet,
|
||||
expressions,
|
||||
schema,
|
||||
} => {
|
||||
let result = execute(input, source)?;
|
||||
let result = execute(input, instance)?;
|
||||
let mut rows = Vec::new();
|
||||
for row in result.rows() {
|
||||
let values = expressions
|
||||
@ -118,14 +95,14 @@ pub fn execute(plan: &LogicalPlan, source: &dyn DataSource) -> Result<ResultSet,
|
||||
keys,
|
||||
schema,
|
||||
} => {
|
||||
let result = execute(input, source)?;
|
||||
let result = execute(input, instance)?;
|
||||
let mut rows = result.rows().to_vec();
|
||||
let resolved_keys = resolve_sort_keys(keys, result.schema())?;
|
||||
rows.sort_by(|left, right| compare_rows(left, right, &resolved_keys));
|
||||
Ok(ResultSet::new(schema.clone(), rows))
|
||||
}
|
||||
LogicalPlan::Limit { input, count } => {
|
||||
let result = execute(input, source)?;
|
||||
let result = execute(input, instance)?;
|
||||
let rows = result.rows().iter().take(*count).cloned().collect();
|
||||
Ok(ResultSet::new(result.schema().clone(), rows))
|
||||
}
|
||||
@ -242,43 +219,3 @@ fn compare_values(left: &Value, right: &Value) -> Ordering {
|
||||
(Value::Boolean(_), Value::Text(_)) => Ordering::Greater,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chase::{Atom, Term};
|
||||
use crate::relational::{DataType, Field};
|
||||
|
||||
#[test]
|
||||
fn instance_datasource_scans_predicate_as_table() {
|
||||
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 schema = Schema::new(vec![
|
||||
Field::new("c0", DataType::Text, false),
|
||||
Field::new("c1", DataType::Text, false),
|
||||
]);
|
||||
|
||||
let result = instance.scan("Parent", &schema).unwrap();
|
||||
assert_eq!(result.rows().len(), 2);
|
||||
assert_eq!(result.schema().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instance_datasource_returns_empty_for_unknown_predicate() {
|
||||
let instance = Instance::new();
|
||||
let schema = Schema::new(vec![]);
|
||||
let result = instance.scan("Missing", &schema).unwrap();
|
||||
assert_eq!(result.rows().len(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,72 +0,0 @@
|
||||
//! An in-memory table store backed by hash maps.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::relational::{ResultSet, Row, Schema};
|
||||
|
||||
use super::{DataSource, ExecutionError};
|
||||
|
||||
/// A simple in-memory data source backed by named tables of rows.
|
||||
///
|
||||
/// Unlike [`Instance`](crate::chase::Instance), which stores chase-level atoms,
|
||||
/// `TableStore` holds typed relational rows directly. This makes it useful for
|
||||
/// testing and for scenarios where data does not originate from the chase engine.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TableStore {
|
||||
tables: HashMap<String, (Schema, Vec<Row>)>,
|
||||
}
|
||||
|
||||
impl TableStore {
|
||||
/// Create an empty table store.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Register a table with its schema and initial rows.
|
||||
pub fn insert(&mut self, name: impl Into<String>, schema: Schema, rows: Vec<Row>) {
|
||||
self.tables.insert(name.into(), (schema, rows));
|
||||
}
|
||||
}
|
||||
|
||||
impl DataSource for TableStore {
|
||||
fn scan(&self, table: &str, schema: &Schema) -> Result<ResultSet, ExecutionError> {
|
||||
match self.tables.get(table) {
|
||||
Some((_, rows)) => Ok(ResultSet::new(schema.clone(), rows.clone())),
|
||||
None => Ok(ResultSet::new(schema.clone(), Vec::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::relational::{DataType, Field, Value};
|
||||
|
||||
#[test]
|
||||
fn scans_registered_table() {
|
||||
let schema = Schema::new(vec![
|
||||
Field::new("name", DataType::Text, false),
|
||||
Field::new("age", DataType::Integer, false),
|
||||
]);
|
||||
|
||||
let rows = vec![
|
||||
Row::new(vec![Value::text("alice"), Value::Integer(30)]),
|
||||
Row::new(vec![Value::text("bob"), Value::Integer(25)]),
|
||||
];
|
||||
|
||||
let mut store = TableStore::new();
|
||||
store.insert("people", schema.clone(), rows);
|
||||
|
||||
let result = store.scan("people", &schema).unwrap();
|
||||
assert_eq!(result.rows().len(), 2);
|
||||
assert_eq!(result.schema().fields()[0].name(), "name");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_empty_for_missing_table() {
|
||||
let store = TableStore::new();
|
||||
let schema = Schema::new(vec![]);
|
||||
let result = store.scan("missing", &schema).unwrap();
|
||||
assert_eq!(result.rows().len(), 0);
|
||||
}
|
||||
}
|
||||
@ -329,34 +329,3 @@ fn select_where_not_equal_excludes_matching_rows() {
|
||||
assert_eq!(result.rows().len(), 1);
|
||||
assert_eq!(format!("{}", result.rows()[0].values()[0]), "bob");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execute_with_table_store_scans_in_memory_rows() {
|
||||
use query_engine::execution::TableStore;
|
||||
use query_engine::relational::{DataType, Field, Row, Schema, Value};
|
||||
|
||||
let schema = Schema::new(vec![
|
||||
Field::new("name", DataType::Text, false),
|
||||
Field::new("age", DataType::Integer, false),
|
||||
]);
|
||||
|
||||
let mut store = TableStore::new();
|
||||
store.insert(
|
||||
"people",
|
||||
schema.clone(),
|
||||
vec![
|
||||
Row::new(vec![Value::text("alice"), Value::Integer(30)]),
|
||||
Row::new(vec![Value::text("bob"), Value::Integer(25)]),
|
||||
],
|
||||
);
|
||||
|
||||
let mut catalog = PredicateCatalog::new();
|
||||
catalog.register_table("people", schema);
|
||||
|
||||
let select = parse_select("SELECT name FROM people WHERE age != 30").unwrap();
|
||||
let plan = plan_select(&select, &catalog).unwrap();
|
||||
let result = execute(&plan, &store).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