use std::fmt;
use rudb_common::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Cardinality {
ExactlyOne,
AtMostOne,
Unverified,
}
impl Cardinality {
#[must_use]
pub fn links(self) -> bool {
matches!(self, Self::ExactlyOne | Self::AtMostOne)
}
#[must_use]
pub fn total(self) -> bool {
matches!(self, Self::ExactlyOne)
}
#[must_use]
pub fn tag(self) -> u8 {
match self {
Self::ExactlyOne => 0,
Self::AtMostOne => 1,
Self::Unverified => 2,
}
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::ExactlyOne => "exactly one",
Self::AtMostOne => "at most one",
Self::Unverified => "unverified",
}
}
pub fn from_tag(tag: u8) -> Result<Self> {
match tag {
0 => Ok(Self::ExactlyOne),
1 => Ok(Self::AtMostOne),
2 => Ok(Self::Unverified),
_ => Err(malformed(format!("cardinality {tag} is not one this build knows"))),
}
}
}
impl fmt::Display for Cardinality {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.label())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Side {
pub table: String,
pub columns: Vec<String>,
}
impl Side {
pub fn new(table: impl Into<String>, column: impl Into<String>) -> Self {
Self { table: table.into(), columns: vec![column.into()] }
}
pub fn composite(table: impl Into<String>, columns: Vec<String>) -> Result<Self> {
if columns.is_empty() {
return Err(malformed("a relationship side needs at least one key column"));
}
Ok(Self { table: table.into(), columns })
}
}
impl fmt::Display for Side {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}({})", self.table, self.columns.join(", "))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Relationship {
pub child: Side,
pub parent: Side,
pub cardinality: Cardinality,
}
impl Relationship {
pub fn declare(child: Side, parent: Side) -> Result<Self> {
if child.columns.len() != parent.columns.len() {
return Err(malformed(format!(
"the child key {child} has {} columns and the parent key {parent} has {}",
child.columns.len(),
parent.columns.len()
)));
}
if child.table == parent.table && child.columns == parent.columns {
return Err(malformed(format!("{child} references itself through its own columns")));
}
Ok(Self { child, parent, cardinality: Cardinality::Unverified })
}
#[must_use]
pub fn name(&self) -> String {
format!("{} -> {}", self.child, self.parent)
}
#[must_use]
pub fn single_column(&self) -> bool {
self.child.columns.len() == 1
}
}
impl fmt::Display for Relationship {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{} -> {}", self.child, self.parent)
}
}
pub fn parse_links(setting: &str) -> Result<Vec<Relationship>> {
let mut links = Vec::new();
for entry in setting.split(',').map(str::trim) {
if entry.is_empty() {
continue;
}
links.push(entry.to_owned());
}
let mut joined: Vec<String> = Vec::new();
for piece in links {
match joined.last_mut() {
Some(open) if !balanced(open) => {
open.push_str(", ");
open.push_str(&piece);
}
_ => joined.push(piece),
}
}
let mut parsed = Vec::with_capacity(joined.len());
for entry in &joined {
parsed.push(parse_link(entry)?);
}
Ok(parsed)
}
fn balanced(entry: &str) -> bool {
let opens = entry.matches('(').count();
let closes = entry.matches(')').count();
opens == closes && entry.contains("->") && closes == 2
}
fn parse_link(entry: &str) -> Result<Relationship> {
let Some((child, parent)) = entry.split_once("->") else {
return Err(malformed(format!(
"expected `child(column) -> parent(column)` and found `{entry}`"
)));
};
Relationship::declare(parse_side(child.trim())?, parse_side(parent.trim())?)
}
fn parse_side(side: &str) -> Result<Side> {
let Some((table, rest)) = side.split_once('(') else {
return Err(malformed(format!("expected `table(column)` and found `{side}`")));
};
let Some(columns) = rest.strip_suffix(')') else {
return Err(malformed(format!("`{side}` is missing its closing parenthesis")));
};
let table = table.trim();
if table.is_empty() {
return Err(malformed(format!("`{side}` names no table")));
}
let columns: Vec<String> = columns
.split(',')
.map(str::trim)
.filter(|column| !column.is_empty())
.map(str::to_owned)
.collect();
Side::composite(table, columns)
}
fn malformed(message: impl Into<String>) -> Error {
Error::invalid_input(format!("invalid rudb relationship: {}", message.into()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_eight_tpch_relationships_parse_into_eight_relationships() {
let setting = "nation(n_regionkey) -> region(r_regionkey), \
supplier(s_nationkey) -> nation(n_nationkey), \
customer(c_nationkey) -> nation(n_nationkey), \
partsupp(ps_partkey) -> part(p_partkey), \
partsupp(ps_suppkey) -> supplier(s_suppkey), \
orders(o_custkey) -> customer(c_custkey), \
lineitem(l_orderkey) -> orders(o_orderkey), \
lineitem(l_partkey) -> part(p_partkey)";
let links = parse_links(setting).expect("parse");
assert_eq!(links.len(), 8);
assert_eq!(links[0].child, Side::new("nation", "n_regionkey"));
assert_eq!(links[0].parent, Side::new("region", "r_regionkey"));
assert_eq!(links[7].name(), "lineitem(l_partkey) -> part(p_partkey)");
assert!(links.iter().all(Relationship::single_column));
assert!(
links.iter().all(|link| link.cardinality == Cardinality::Unverified),
"a declaration is unverified until a build has looked at the column"
);
}
#[test]
fn a_composite_key_survives_the_comma_that_separates_links() {
let setting = "child(a, b) -> parent(c, d), other(e) -> parent2(f)";
let links = parse_links(setting).expect("parse");
assert_eq!(links.len(), 2);
assert_eq!(links[0].child.columns, vec!["a".to_owned(), "b".to_owned()]);
assert_eq!(links[0].parent.columns, vec!["c".to_owned(), "d".to_owned()]);
assert!(!links[0].single_column());
assert_eq!(links[1].child.columns, vec!["e".to_owned()]);
}
#[test]
fn whitespace_and_a_trailing_comma_are_free() {
let setting = " orders( o_custkey ) -> customer( c_custkey ) , ";
let links = parse_links(setting).expect("parse");
assert_eq!(links.len(), 1);
assert_eq!(links[0].child, Side::new("orders", "o_custkey"));
}
#[test]
fn an_empty_setting_declares_nothing_rather_than_failing() {
assert!(parse_links("").expect("parse").is_empty());
assert!(parse_links(" ").expect("parse").is_empty());
}
#[test]
fn a_link_with_no_arrow_is_refused_and_says_what_was_expected() {
let error = parse_links("orders(o_custkey) customer(c_custkey)").expect_err("refused");
let text = error.to_string();
assert!(text.contains("child(column) -> parent(column)"), "{text}");
}
#[test]
fn a_side_with_no_parenthesis_is_refused() {
assert!(parse_links("orders -> customer(c_custkey)").is_err());
assert!(parse_links("orders(o_custkey -> customer(c_custkey)").is_err());
}
#[test]
fn a_side_with_no_table_is_refused() {
assert!(parse_links("(o_custkey) -> customer(c_custkey)").is_err());
}
#[test]
fn a_side_with_no_columns_is_refused() {
assert!(parse_links("orders() -> customer(c_custkey)").is_err());
}
#[test]
fn a_declaration_whose_sides_have_different_widths_is_refused() {
let error = parse_links("child(a, b) -> parent(c)").expect_err("refused");
assert!(error.to_string().contains("columns"), "{error}");
}
#[test]
fn a_table_referencing_itself_through_its_own_columns_is_refused() {
let error = parse_links("orders(o_orderkey) -> orders(o_orderkey)").expect_err("refused");
assert!(error.to_string().contains("itself"), "{error}");
}
#[test]
fn a_table_referencing_itself_through_a_different_column_is_allowed() {
let links = parse_links("employee(manager) -> employee(id)").expect("parse");
assert_eq!(links.len(), 1);
}
#[test]
fn only_exactly_one_licenses_the_rewrites_that_need_a_parent_for_every_child() {
assert!(Cardinality::ExactlyOne.links());
assert!(Cardinality::ExactlyOne.total());
assert!(Cardinality::AtMostOne.links());
assert!(!Cardinality::AtMostOne.total(), "a null key matches no parent row");
assert!(!Cardinality::Unverified.links(), "an unverified side takes an ordinary join");
assert!(!Cardinality::Unverified.total());
}
#[test]
fn the_cardinality_tag_round_trips_and_an_unknown_one_is_refused() {
for cardinality in
[Cardinality::ExactlyOne, Cardinality::AtMostOne, Cardinality::Unverified]
{
assert_eq!(Cardinality::from_tag(cardinality.tag()).expect("a known tag"), cardinality);
}
assert!(Cardinality::from_tag(7).is_err());
}
#[test]
fn many_to_many_is_declared_as_two_many_to_one_through_the_link_table() {
let links = parse_links(
"partsupp(ps_partkey) -> part(p_partkey), partsupp(ps_suppkey) -> supplier(s_suppkey)",
)
.expect("parse");
assert_eq!(links.len(), 2);
assert_eq!(links[0].child.table, "partsupp");
assert_eq!(links[1].child.table, "partsupp");
}
}