Compare commits

...

2 Commits

Author SHA1 Message Date
Hassan Abedi
b29141c47c Up 2026-06-03 12:02:14 +02:00
Hassan Abedi
0b89e64773 WIP 2026-06-03 11:55:38 +02:00
2 changed files with 128 additions and 76 deletions

View File

@ -1,9 +1,7 @@
## Query Ops ## Query Ops
Physical operators for a small query-plan executor: atom scan, semijoin, and natural join over a binding relation. This crate provides a small set of query operators that can be used to implement a simple query-plan executor.
Operators compose by function application, so a query plan written by hand is just an expression. The operators are: atom scan, semijoin, and natural join.
![Architecture Diagram](docs/diagrams/architecture.svg)
### Public API ### Public API
@ -20,41 +18,81 @@ Operators compose by function application, so a query plan written by hand is ju
### Example ### Example
`Q(X) :- edge(X, X), labeled(X).` (labeled self-loops): The rule below returns the authors of every bestseller along with the book's price.
It uses all three operators: `scan_atom` for the three input tables, `semijoin` to keep only authors of bestsellers, and `natural_join` to attach each
book's price.
```text
Q(name, book, dollars) :- author(name, book), bestseller(book), price(book, dollars).
```
```rust ```rust
use query_ops::atom::{AtomPattern, Term, scan_atom}; use query_ops::atom::{AtomPattern, Term, scan_atom};
use query_ops::join::semijoin; use query_ops::join::{natural_join, semijoin};
use query_ops::table::Table; use query_ops::table::Table;
use query_ops::value::Value; use query_ops::value::Value;
fn s(x: &str) -> Value {
Value::Str(x.to_string())
}
fn i(x: i64) -> Value {
Value::Int(x)
}
fn main() { fn main() {
let edge = Table::from_rows( let author = Table::from_rows(
2, 2,
vec![ vec![
vec![Value::Int(1), Value::Int(2)], vec![s("Alice"), s("Foo")],
vec![Value::Int(3), Value::Int(3)], // self-loop on 3 vec![s("Bob"), s("Bar")],
vec![Value::Int(2), Value::Int(2)], // self-loop on 2 vec![s("Alice"), s("Baz")],
vec![s("Carol"), s("Qux")],
],
);
let bestseller = Table::from_rows(1, vec![vec![s("Foo")], vec![s("Baz")]]);
let price = Table::from_rows(
2,
vec![
vec![s("Foo"), i(25)],
vec![s("Bar"), i(15)],
vec![s("Baz"), i(30)],
vec![s("Qux"), i(20)],
], ],
); );
let labeled = Table::from_rows(1, vec![vec![Value::Int(2)]]);
let self_loops = scan_atom( let author_rel = scan_atom(
&edge, &author,
&AtomPattern { &AtomPattern {
columns: vec![Term::Var("X".to_string()), Term::Var("X".to_string())], columns: vec![Term::Var("name".to_string()), Term::Var("book".to_string())],
}, },
); );
let labeled_x = scan_atom( let bestseller_rel = scan_atom(
&labeled, &bestseller,
&AtomPattern { &AtomPattern {
columns: vec![Term::Var("X".to_string())], columns: vec![Term::Var("book".to_string())],
},
);
let price_rel = scan_atom(
&price,
&AtomPattern {
columns: vec![Term::Var("book".to_string()), Term::Var("dollars".to_string())],
}, },
); );
let result = semijoin(&self_loops, &labeled_x);
assert_eq!(result.columns, vec!["X".to_string()]); let authors_of_bestsellers = semijoin(&author_rel, &bestseller_rel);
assert_eq!(result.rows, vec![vec![Value::Int(2)]]); let result = natural_join(&authors_of_bestsellers, &price_rel);
assert_eq!(
result.columns,
vec!["name".to_string(), "book".to_string(), "dollars".to_string()],
);
assert_eq!(
result.rows,
vec![
vec![s("Alice"), s("Foo"), i(25)],
vec![s("Alice"), s("Baz"), i(30)],
],
);
} }
``` ```

View File

@ -1,77 +1,91 @@
//! Hand-written query plans composed from `scan_atom`, `semijoin`, and `natural_join`. //! Hand-written query plan composed from `scan_atom`, `semijoin`, and `natural_join`.
//! //!
//! Schema: //! Schema:
//! - `edge(src, dst)`: directed edges //! - `author(name, book)`: who wrote each book
//! - `labeled(node)`: a set of labeled nodes //! - `bestseller(book)`: the set of bestseller titles
//! - `price(book, dollars)`: price of each book
//! //!
//! Two rules are executed against the same fixture: //! Rule:
//! - `Q1(X) :- edge(X, X), labeled(X).` (labeled self-loops) //! - `Q(name, book, dollars) :- author(name, book), bestseller(book), price(book, dollars).`
//! - `Q2(X, Y) :- edge(X, Y), labeled(Y).` (edges whose destination is labeled) //! ("Authors of bestsellers along with each book's price.")
//!
//! The plan first scans each input table, then narrows `author` to authors of
//! bestsellers via a semijoin against `bestseller`, then attaches each book's
//! price via a natural join against `price`.
use query_ops::atom::{scan_atom, AtomPattern, Term}; use query_ops::atom::{AtomPattern, Term, scan_atom};
use query_ops::join::{natural_join, semijoin}; use query_ops::join::{natural_join, semijoin};
use query_ops::table::Table; use query_ops::table::Table;
use query_ops::value::Value; use query_ops::value::Value;
fn var(name: &str) -> Term { fn s(x: &str) -> Value {
Term::Var(name.to_string()) Value::Str(x.to_string())
} }
fn int(value: i64) -> Value { fn i(x: i64) -> Value {
Value::Int(value) Value::Int(x)
} }
#[test] #[test]
fn labeled_self_loops_and_edges_into_labeled_nodes() { fn authors_of_bestsellers_with_price() {
let edge = Table::from_rows( let author = Table::from_rows(
2, 2,
vec![ vec![
vec![int(1), int(2)], vec![s("Alice"), s("Foo")],
vec![int(2), int(3)], vec![s("Bob"), s("Bar")],
vec![int(3), int(3)], vec![s("Alice"), s("Baz")],
vec![int(4), int(1)], vec![s("Carol"), s("Qux")],
vec![int(2), int(2)],
], ],
); );
let labeled = Table::from_rows(1, vec![vec![int(2)], vec![int(3)]]); let bestseller = Table::from_rows(1, vec![vec![s("Foo")], vec![s("Baz")]]);
let price = Table::from_rows(
let self_loops = scan_atom( 2,
&edge,
&AtomPattern {
columns: vec![var("X"), var("X")],
},
);
let labeled_x = scan_atom(
&labeled,
&AtomPattern {
columns: vec![var("X")],
},
);
let q1 = semijoin(&self_loops, &labeled_x);
assert_eq!(q1.columns, vec!["X".to_string()]);
assert_eq!(q1.rows, vec![vec![int(3)], vec![int(2)]]);
let edge_xy = scan_atom(
&edge,
&AtomPattern {
columns: vec![var("X"), var("Y")],
},
);
let labeled_y = scan_atom(
&labeled,
&AtomPattern {
columns: vec![var("Y")],
},
);
let q2 = natural_join(&edge_xy, &labeled_y);
assert_eq!(q2.columns, vec!["X".to_string(), "Y".to_string()]);
assert_eq!(
q2.rows,
vec![ vec![
vec![int(1), int(2)], vec![s("Foo"), i(25)],
vec![int(2), int(3)], vec![s("Bar"), i(15)],
vec![int(3), int(3)], vec![s("Baz"), i(30)],
vec![int(2), int(2)], vec![s("Qux"), i(20)],
],
);
let author_rel = scan_atom(
&author,
&AtomPattern {
columns: vec![Term::Var("name".to_string()), Term::Var("book".to_string())],
},
);
let bestseller_rel = scan_atom(
&bestseller,
&AtomPattern {
columns: vec![Term::Var("book".to_string())],
},
);
let price_rel = scan_atom(
&price,
&AtomPattern {
columns: vec![
Term::Var("book".to_string()),
Term::Var("dollars".to_string()),
],
},
);
let authors_of_bestsellers = semijoin(&author_rel, &bestseller_rel);
let result = natural_join(&authors_of_bestsellers, &price_rel);
assert_eq!(
result.columns,
vec![
"name".to_string(),
"book".to_string(),
"dollars".to_string()
],
);
assert_eq!(
result.rows,
vec![
vec![s("Alice"), s("Foo"), i(25)],
vec![s("Alice"), s("Baz"), i(30)],
], ],
); );
} }