1use std::fmt;
20
21use rudb_common::{Error, Result};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum Cardinality {
30 ExactlyOne,
34 AtMostOne,
37 Unverified,
41}
42
43impl Cardinality {
44 #[must_use]
46 pub fn links(self) -> bool {
47 matches!(self, Self::ExactlyOne | Self::AtMostOne)
48 }
49
50 #[must_use]
53 pub fn total(self) -> bool {
54 matches!(self, Self::ExactlyOne)
55 }
56
57 #[must_use]
59 pub fn tag(self) -> u8 {
60 match self {
61 Self::ExactlyOne => 0,
62 Self::AtMostOne => 1,
63 Self::Unverified => 2,
64 }
65 }
66
67 #[must_use]
69 pub fn label(self) -> &'static str {
70 match self {
71 Self::ExactlyOne => "exactly one",
72 Self::AtMostOne => "at most one",
73 Self::Unverified => "unverified",
74 }
75 }
76
77 pub fn from_tag(tag: u8) -> Result<Self> {
84 match tag {
85 0 => Ok(Self::ExactlyOne),
86 1 => Ok(Self::AtMostOne),
87 2 => Ok(Self::Unverified),
88 _ => Err(malformed(format!("cardinality {tag} is not one this build knows"))),
89 }
90 }
91}
92
93impl fmt::Display for Cardinality {
94 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
95 formatter.write_str(self.label())
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct Side {
102 pub table: String,
104 pub columns: Vec<String>,
107}
108
109impl Side {
110 pub fn new(table: impl Into<String>, column: impl Into<String>) -> Self {
112 Self { table: table.into(), columns: vec![column.into()] }
113 }
114
115 pub fn composite(table: impl Into<String>, columns: Vec<String>) -> Result<Self> {
122 if columns.is_empty() {
123 return Err(malformed("a relationship side needs at least one key column"));
124 }
125 Ok(Self { table: table.into(), columns })
126 }
127}
128
129impl fmt::Display for Side {
130 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
131 write!(formatter, "{}({})", self.table, self.columns.join(", "))
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct Relationship {
138 pub child: Side,
140 pub parent: Side,
142 pub cardinality: Cardinality,
144}
145
146impl Relationship {
147 pub fn declare(child: Side, parent: Side) -> Result<Self> {
156 if child.columns.len() != parent.columns.len() {
157 return Err(malformed(format!(
158 "the child key {child} has {} columns and the parent key {parent} has {}",
159 child.columns.len(),
160 parent.columns.len()
161 )));
162 }
163 if child.table == parent.table && child.columns == parent.columns {
164 return Err(malformed(format!("{child} references itself through its own columns")));
165 }
166 Ok(Self { child, parent, cardinality: Cardinality::Unverified })
167 }
168
169 #[must_use]
176 pub fn name(&self) -> String {
177 format!("{} -> {}", self.child, self.parent)
178 }
179
180 #[must_use]
182 pub fn single_column(&self) -> bool {
183 self.child.columns.len() == 1
184 }
185}
186
187impl fmt::Display for Relationship {
188 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
189 write!(formatter, "{} -> {}", self.child, self.parent)
190 }
191}
192
193pub fn parse_links(setting: &str) -> Result<Vec<Relationship>> {
219 let mut links = Vec::new();
220 for entry in setting.split(',').map(str::trim) {
221 if entry.is_empty() {
225 continue;
226 }
227 links.push(entry.to_owned());
228 }
229 let mut joined: Vec<String> = Vec::new();
232 for piece in links {
233 match joined.last_mut() {
234 Some(open) if !balanced(open) => {
235 open.push_str(", ");
236 open.push_str(&piece);
237 }
238 _ => joined.push(piece),
239 }
240 }
241
242 let mut parsed = Vec::with_capacity(joined.len());
243 for entry in &joined {
244 parsed.push(parse_link(entry)?);
245 }
246 Ok(parsed)
247}
248
249fn balanced(entry: &str) -> bool {
250 let opens = entry.matches('(').count();
251 let closes = entry.matches(')').count();
252 opens == closes && entry.contains("->") && closes == 2
253}
254
255fn parse_link(entry: &str) -> Result<Relationship> {
256 let Some((child, parent)) = entry.split_once("->") else {
257 return Err(malformed(format!(
258 "expected `child(column) -> parent(column)` and found `{entry}`"
259 )));
260 };
261 Relationship::declare(parse_side(child.trim())?, parse_side(parent.trim())?)
262}
263
264fn parse_side(side: &str) -> Result<Side> {
265 let Some((table, rest)) = side.split_once('(') else {
266 return Err(malformed(format!("expected `table(column)` and found `{side}`")));
267 };
268 let Some(columns) = rest.strip_suffix(')') else {
269 return Err(malformed(format!("`{side}` is missing its closing parenthesis")));
270 };
271 let table = table.trim();
272 if table.is_empty() {
273 return Err(malformed(format!("`{side}` names no table")));
274 }
275 let columns: Vec<String> = columns
276 .split(',')
277 .map(str::trim)
278 .filter(|column| !column.is_empty())
279 .map(str::to_owned)
280 .collect();
281 Side::composite(table, columns)
282}
283
284fn malformed(message: impl Into<String>) -> Error {
285 Error::invalid_input(format!("invalid rudb relationship: {}", message.into()))
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 #[test]
293 fn the_eight_tpch_relationships_parse_into_eight_relationships() {
294 let setting = "nation(n_regionkey) -> region(r_regionkey), \
297 supplier(s_nationkey) -> nation(n_nationkey), \
298 customer(c_nationkey) -> nation(n_nationkey), \
299 partsupp(ps_partkey) -> part(p_partkey), \
300 partsupp(ps_suppkey) -> supplier(s_suppkey), \
301 orders(o_custkey) -> customer(c_custkey), \
302 lineitem(l_orderkey) -> orders(o_orderkey), \
303 lineitem(l_partkey) -> part(p_partkey)";
304 let links = parse_links(setting).expect("parse");
305 assert_eq!(links.len(), 8);
306 assert_eq!(links[0].child, Side::new("nation", "n_regionkey"));
307 assert_eq!(links[0].parent, Side::new("region", "r_regionkey"));
308 assert_eq!(links[7].name(), "lineitem(l_partkey) -> part(p_partkey)");
309 assert!(links.iter().all(Relationship::single_column));
310 assert!(
311 links.iter().all(|link| link.cardinality == Cardinality::Unverified),
312 "a declaration is unverified until a build has looked at the column"
313 );
314 }
315
316 #[test]
317 fn a_composite_key_survives_the_comma_that_separates_links() {
318 let setting = "child(a, b) -> parent(c, d), other(e) -> parent2(f)";
322 let links = parse_links(setting).expect("parse");
323 assert_eq!(links.len(), 2);
324 assert_eq!(links[0].child.columns, vec!["a".to_owned(), "b".to_owned()]);
325 assert_eq!(links[0].parent.columns, vec!["c".to_owned(), "d".to_owned()]);
326 assert!(!links[0].single_column());
327 assert_eq!(links[1].child.columns, vec!["e".to_owned()]);
328 }
329
330 #[test]
331 fn whitespace_and_a_trailing_comma_are_free() {
332 let setting = " orders( o_custkey ) -> customer( c_custkey ) , ";
333 let links = parse_links(setting).expect("parse");
334 assert_eq!(links.len(), 1);
335 assert_eq!(links[0].child, Side::new("orders", "o_custkey"));
336 }
337
338 #[test]
339 fn an_empty_setting_declares_nothing_rather_than_failing() {
340 assert!(parse_links("").expect("parse").is_empty());
341 assert!(parse_links(" ").expect("parse").is_empty());
342 }
343
344 #[test]
345 fn a_link_with_no_arrow_is_refused_and_says_what_was_expected() {
346 let error = parse_links("orders(o_custkey) customer(c_custkey)").expect_err("refused");
347 let text = error.to_string();
348 assert!(text.contains("child(column) -> parent(column)"), "{text}");
349 }
350
351 #[test]
352 fn a_side_with_no_parenthesis_is_refused() {
353 assert!(parse_links("orders -> customer(c_custkey)").is_err());
354 assert!(parse_links("orders(o_custkey -> customer(c_custkey)").is_err());
355 }
356
357 #[test]
358 fn a_side_with_no_table_is_refused() {
359 assert!(parse_links("(o_custkey) -> customer(c_custkey)").is_err());
360 }
361
362 #[test]
363 fn a_side_with_no_columns_is_refused() {
364 assert!(parse_links("orders() -> customer(c_custkey)").is_err());
365 }
366
367 #[test]
368 fn a_declaration_whose_sides_have_different_widths_is_refused() {
369 let error = parse_links("child(a, b) -> parent(c)").expect_err("refused");
372 assert!(error.to_string().contains("columns"), "{error}");
373 }
374
375 #[test]
376 fn a_table_referencing_itself_through_its_own_columns_is_refused() {
377 let error = parse_links("orders(o_orderkey) -> orders(o_orderkey)").expect_err("refused");
378 assert!(error.to_string().contains("itself"), "{error}");
379 }
380
381 #[test]
382 fn a_table_referencing_itself_through_a_different_column_is_allowed() {
383 let links = parse_links("employee(manager) -> employee(id)").expect("parse");
386 assert_eq!(links.len(), 1);
387 }
388
389 #[test]
390 fn only_exactly_one_licenses_the_rewrites_that_need_a_parent_for_every_child() {
391 assert!(Cardinality::ExactlyOne.links());
392 assert!(Cardinality::ExactlyOne.total());
393 assert!(Cardinality::AtMostOne.links());
394 assert!(!Cardinality::AtMostOne.total(), "a null key matches no parent row");
395 assert!(!Cardinality::Unverified.links(), "an unverified side takes an ordinary join");
396 assert!(!Cardinality::Unverified.total());
397 }
398
399 #[test]
400 fn the_cardinality_tag_round_trips_and_an_unknown_one_is_refused() {
401 for cardinality in
402 [Cardinality::ExactlyOne, Cardinality::AtMostOne, Cardinality::Unverified]
403 {
404 assert_eq!(Cardinality::from_tag(cardinality.tag()).expect("a known tag"), cardinality);
405 }
406 assert!(Cardinality::from_tag(7).is_err());
407 }
408
409 #[test]
410 fn many_to_many_is_declared_as_two_many_to_one_through_the_link_table() {
411 let links = parse_links(
415 "partsupp(ps_partkey) -> part(p_partkey), partsupp(ps_suppkey) -> supplier(s_suppkey)",
416 )
417 .expect("parse");
418 assert_eq!(links.len(), 2);
419 assert_eq!(links[0].child.table, "partsupp");
420 assert_eq!(links[1].child.table, "partsupp");
421 }
422}