diff --git a/crates/query-ops/README.md b/crates/query-ops/README.md index 7068596..34bc79a 100644 --- a/crates/query-ops/README.md +++ b/crates/query-ops/README.md @@ -18,41 +18,81 @@ The operators are: atom scan, semijoin, and natural join. ### 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 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::value::Value; +fn s(x: &str) -> Value { + Value::Str(x.to_string()) +} +fn i(x: i64) -> Value { + Value::Int(x) +} + fn main() { - let edge = Table::from_rows( + let author = Table::from_rows( 2, vec![ - vec![Value::Int(1), Value::Int(2)], - vec![Value::Int(3), Value::Int(3)], // self-loop on 3 - vec![Value::Int(2), Value::Int(2)], // self-loop on 2 + vec![s("Alice"), s("Foo")], + vec![s("Bob"), s("Bar")], + 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( - &edge, + let author_rel = scan_atom( + &author, &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( - &labeled, + let bestseller_rel = scan_atom( + &bestseller, &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()]); - assert_eq!(result.rows, vec![vec![Value::Int(2)]]); + 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)], + ], + ); } ``` diff --git a/crates/query-ops/tests/hand_plan.rs b/crates/query-ops/tests/hand_plan.rs index a23c1b1..7265610 100644 --- a/crates/query-ops/tests/hand_plan.rs +++ b/crates/query-ops/tests/hand_plan.rs @@ -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: -//! - `edge(src, dst)`: directed edges -//! - `labeled(node)`: a set of labeled nodes +//! - `author(name, book)`: who wrote each book +//! - `bestseller(book)`: the set of bestseller titles +//! - `price(book, dollars)`: price of each book //! -//! Two rules are executed against the same fixture: -//! - `Q1(X) :- edge(X, X), labeled(X).` (labeled self-loops) -//! - `Q2(X, Y) :- edge(X, Y), labeled(Y).` (edges whose destination is labeled) +//! Rule: +//! - `Q(name, book, dollars) :- author(name, book), bestseller(book), price(book, dollars).` +//! ("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::table::Table; use query_ops::value::Value; -fn var(name: &str) -> Term { - Term::Var(name.to_string()) +fn s(x: &str) -> Value { + Value::Str(x.to_string()) } -fn int(value: i64) -> Value { - Value::Int(value) +fn i(x: i64) -> Value { + Value::Int(x) } #[test] -fn labeled_self_loops_and_edges_into_labeled_nodes() { - let edge = Table::from_rows( +fn authors_of_bestsellers_with_price() { + let author = Table::from_rows( 2, vec![ - vec![int(1), int(2)], - vec![int(2), int(3)], - vec![int(3), int(3)], - vec![int(4), int(1)], - vec![int(2), int(2)], + vec![s("Alice"), s("Foo")], + vec![s("Bob"), s("Bar")], + vec![s("Alice"), s("Baz")], + vec![s("Carol"), s("Qux")], ], ); - let labeled = Table::from_rows(1, vec![vec![int(2)], vec![int(3)]]); - - let self_loops = scan_atom( - &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, + let bestseller = Table::from_rows(1, vec![vec![s("Foo")], vec![s("Baz")]]); + let price = Table::from_rows( + 2, vec![ - vec![int(1), int(2)], - vec![int(2), int(3)], - vec![int(3), int(3)], - vec![int(2), int(2)], + vec![s("Foo"), i(25)], + vec![s("Bar"), i(15)], + vec![s("Baz"), i(30)], + 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)], ], ); }