rudb_exec/links.rs
1//! `rudb_links()`, the table that says what the graph layer knows about this database.
2//!
3//! One row per declared relationship. What is declared comes from the tables' foreign keys and from
4//! the session, because `SET graph_links` is where a relationship is declared for tables that
5//! arrived from Parquet and Parquet has no foreign keys to read one out of. What is stored comes from the tables themselves,
6//! so a row is a declaration on the left and a measurement on the right, and the two are never
7//! mixed: section 2.3 of spec/graph/02-the-data-model.md says a declaration is what somebody
8//! believes and a build is what is true.
9//!
10//! A reader asks this one of four questions. Is my relationship known at all, which is whether it
11//! has a row. Was it verified, which is the cardinality column and is the difference between a join
12//! that can use a structure and one that has to hash. What does it cost, which is the bytes, filled
13//! whether or not the structure was kept so that section 3.7's budget is a number somebody can act
14//! on rather than a silence. And what shape did it turn out to have, which is the degree columns
15//! and is the only group here that says something about the data rather than about the file.
16
17use rudb_catalog::{Catalog, Rows, Table};
18use rudb_common::{Result, Session, Value};
19use rudb_functions::link_fields;
20use rudb_graph::{Cardinality, Degrees, Relationship, Side, parse_links};
21use rudb_native::graph::Edge;
22use rudb_plan::{Plan, Slice};
23
24use crate::metadata::{Metadata, text};
25
26/// Every declared relationship and what is stored for it, in the columns the plan asked for.
27///
28/// # Errors
29///
30/// If the plan asks for a column this table does not have.
31pub(crate) fn links(
32 session: &Session,
33 catalog: &Catalog,
34 plan: &Plan,
35 index: u32,
36 columns: Slice,
37) -> Result<Metadata> {
38 // The setting was parsed once when it was set, so this cannot fail on anything a `SET` let
39 // through. A session filled in by something other than the settings layer is the other caller,
40 // and a declaration it could not parse is one no build will have acted on either, so the table
41 // says nothing about it rather than refusing to be read.
42 let declared = declared(catalog, session.links());
43 let mut rows = Vec::with_capacity(declared.len());
44 for link in &declared {
45 rows.push(row(catalog, link));
46 }
47 Metadata::new("rudb_links", &link_fields(), &rows, plan, index, columns)
48}
49
50/// Every relationship declared for this catalog: the ones `graph_links` names, then one for each
51/// foreign key a table was created with that the setting does not already name.
52///
53/// Section 2.5 of spec/graph/02-the-data-model.md lists the foreign keys first, as the path that
54/// needs nothing from the user, and the setting second, for data that arrived without constraints.
55/// The two are one list because nothing downstream cares where a relationship came from: the build
56/// verifies both the same way and a plan reads neither until it has.
57///
58/// A setting the parser refused contributes nothing, which is the silence `rudb_links()` has always
59/// given one. A foreign key over no columns or into a table of another schema is left out.
60#[must_use]
61pub fn declared(catalog: &Catalog, setting: &str) -> Vec<Relationship> {
62 let mut declared = parse_links(setting).unwrap_or_default();
63 for table in catalog.tables() {
64 for foreign in table.foreign() {
65 if !foreign.table.schema.eq_ignore_ascii_case(&table.name().schema) {
66 continue;
67 }
68 let Ok(parent) = catalog.table(&foreign.table) else { continue };
69 let named = |on: &Table, columns: &[usize]| {
70 columns
71 .iter()
72 .map(|&at| on.columns().get(at).map(|field| field.name.clone()))
73 .collect::<Option<Vec<String>>>()
74 };
75 let (Some(child_columns), Some(parent_columns)) =
76 (named(table, &foreign.columns), named(parent, &foreign.referenced))
77 else {
78 continue;
79 };
80 let (Ok(child), Ok(parent)) = (
81 Side::composite(table.name().table.clone(), child_columns),
82 Side::composite(parent.name().table.clone(), parent_columns),
83 ) else {
84 continue;
85 };
86 let Ok(link) = Relationship::declare(child, parent) else { continue };
87 let named_already = declared.iter().any(|held| {
88 held.child.table.eq_ignore_ascii_case(&link.child.table)
89 && held.parent.table.eq_ignore_ascii_case(&link.parent.table)
90 && same_columns(&held.child.columns, &link.child.columns)
91 && same_columns(&held.parent.columns, &link.parent.columns)
92 });
93 if !named_already {
94 declared.push(link);
95 }
96 }
97 }
98 declared
99}
100
101fn same_columns(left: &[String], right: &[String]) -> bool {
102 left.len() == right.len() && left.iter().zip(right).all(|(l, r)| l.eq_ignore_ascii_case(r))
103}
104
105/// What one relationship reports.
106fn row(catalog: &Catalog, link: &Relationship) -> Vec<Value> {
107 let stored = key_map_of(catalog, &link.parent);
108 let held = forward_link_of(catalog, link);
109 let shape = degrees_of(catalog, link);
110 // A structure is stored, or it was measured and turned away, or nothing has looked at the
111 // column. The first two both have a size and the third does not, so the size column falls back
112 // to the budget record and only goes null for the third.
113 let map_bytes = stored
114 .as_ref()
115 .map(|map| map.bytes as u64)
116 .or_else(|| refused_key_map_of(catalog, &link.parent));
117 let link_bytes =
118 held.as_ref().map(|held| held.bytes() as u64).or_else(|| refused_link_of(catalog, link));
119 let (cardinality, note) = verdict(
120 catalog,
121 link,
122 stored.as_ref(),
123 held.as_ref(),
124 map_bytes.is_some(),
125 link_bytes.is_some(),
126 );
127 vec![
128 text(&link.name()),
129 text(&link.child.table),
130 text(&link.child.columns.join(", ")),
131 text(&link.parent.table),
132 text(&link.parent.columns.join(", ")),
133 text(cardinality),
134 stored.as_ref().map_or(Value::Null, |map| text(map.form.label())),
135 map_bytes.map_or(Value::Null, |bytes| Value::BigInt(clamp(bytes))),
136 held.as_ref().map_or(Value::Null, |held| text(held.form().label())),
137 link_bytes.map_or(Value::Null, |bytes| Value::BigInt(clamp(bytes))),
138 shape.as_ref().map_or(Value::Null, |shape| Value::Double(shape.mean())),
139 shape.as_ref().map_or(Value::Null, |shape| Value::BigInt(clamp(shape.highest()))),
140 shape.as_ref().map_or(Value::Null, |shape| Value::BigInt(clamp(shape.percentile(0.99)))),
141 shape.as_ref().and_then(Degrees::locality).map_or(Value::Null, Value::Double),
142 shape.as_ref().map_or(Value::Null, |shape| Value::Boolean(shape.unique())),
143 shape.as_ref().map_or(Value::Null, |shape| Value::Boolean(shape.total())),
144 note.map_or(Value::Null, text),
145 ]
146}
147
148/// A count that fits in the signed integer the column is.
149///
150/// A degree or a size past nine quintillion is one this codebase will not meet, and saturating is
151/// the right answer for the one place either could come from: the last histogram bucket's bound,
152/// which is already a bound rather than a count.
153fn clamp(count: u64) -> i64 {
154 i64::try_from(count).unwrap_or(i64::MAX)
155}
156
157/// A key map found in a table, reduced to what the table reports.
158struct Stored {
159 form: rudb_graph::Form,
160 bytes: usize,
161 distinct: bool,
162}
163
164/// The stored key map of a relationship's parent side, when there is one this build can read.
165///
166/// One column only. A composite parent key is two key maps over a folded key and nothing folds one
167/// yet, so a composite relationship reports no map rather than the map of its first column, which
168/// would be a true statement about a column and a false one about the relationship.
169fn key_map_of(catalog: &Catalog, parent: &Side) -> Option<Stored> {
170 if parent.columns.len() != 1 {
171 return None;
172 }
173 let table = table_named(catalog, &parent.table)?;
174 let column = table.column_index(&parent.columns[0])?;
175 let Rows::Native(reader) = table.rows() else { return None };
176 let map = rudb_native::graph::key_map(reader, column)?;
177 Some(Stored { form: map.form(), bytes: map.bytes(), distinct: map.observed().distinct })
178}
179
180/// What a key map over a relationship's parent side would have cost, when a build measured one and
181/// did not keep it.
182///
183/// The form it would have taken is on the record too and is not reported. A form column that named
184/// a form no join can read would be the one thing this table does not do, which is to mix what is
185/// believed with what is there. The size is different: it is a fact about a structure that does not
186/// exist, and it is the number somebody raising `graph_budget` needs.
187fn refused_key_map_of(catalog: &Catalog, parent: &Side) -> Option<u64> {
188 if parent.columns.len() != 1 {
189 return None;
190 }
191 let table = table_named(catalog, &parent.table)?;
192 let column = table.column_index(&parent.columns[0])?;
193 let Rows::Native(reader) = table.rows() else { return None };
194 rudb_native::graph::refused_key_map(reader, column).map(|(_, bytes)| bytes)
195}
196
197/// What a forward link over a relationship's child column would have cost, when a build measured
198/// one and did not keep it.
199///
200/// This asks the child table alone, where [`forward_link_of`] checks that the stored link names the
201/// parent this declaration names. There is nothing to check against: the field a stored link keeps
202/// its parent binding in is the field a budget record keeps its size in, so the record says what a
203/// link over this column would have cost and not which parent it was measured against. The record
204/// is keyed by the child column, the way a degree section is, so two declarations over the same
205/// column and different parents read the same size, which is the size of whichever was built last.
206fn refused_link_of(catalog: &Catalog, link: &Relationship) -> Option<u64> {
207 let child = table_named(catalog, &link.child.table)?;
208 let Rows::Native(rows) = child.rows() else { return None };
209 rudb_native::graph::refused_link(rows, key_in(child, &link.child.columns)?)
210 .map(|(_, bytes)| bytes)
211}
212
213/// The stored forward link of a relationship, when both of its tables are in the same file and the
214/// link in the child's sections was built against the parent this declaration names.
215///
216/// Both sides in one file is not a limitation of the format so much as of what a relationship
217/// across two files would mean: a `rid` is a row's position in a table, and a link is a column of
218/// them, so a link stored in one file that names a parent in another would be resolvable only by a
219/// reader that had both open and had checked that neither had moved since. Nothing declares one
220/// today and this reports nothing for one rather than guessing.
221fn forward_link_of(catalog: &Catalog, link: &Relationship) -> Option<rudb_graph::Link> {
222 let child = table_named(catalog, &link.child.table)?;
223 let parent = table_named(catalog, &link.parent.table)?;
224 let (Rows::Native(child_rows), Rows::Native(parent_rows)) = (child.rows(), parent.rows())
225 else {
226 return None;
227 };
228 let edge = Edge {
229 child: child.name().table.clone(),
230 child_column: key_in(child, &link.child.columns)?,
231 parent: parent.name().table.clone(),
232 parent_column: key_in(parent, &link.parent.columns)?,
233 };
234 rudb_native::graph::stored_link(child_rows, parent_rows, &edge)
235}
236
237/// What the link build measured of a relationship's shape, when the child table carries it.
238///
239/// This asks the child table alone, because a degree section is about the child column and has no
240/// parent binding to check. There is no separate check that the link is stored, and there does not
241/// need to be: the build attaches the two together under the same id and stamps them with the same
242/// generation, so a file cannot hold one of them current without the other.
243fn degrees_of(catalog: &Catalog, link: &Relationship) -> Option<Degrees> {
244 let child = table_named(catalog, &link.child.table)?;
245 let Rows::Native(rows) = child.rows() else { return None };
246 rudb_native::graph::stored_degrees(rows, key_in(child, &link.child.columns)?)
247}
248
249/// The number the graph sections name a key over these columns by: the column's index for one, and
250/// `rudb_native::graph::key_of`'s pair for two.
251fn key_in(table: &Table, columns: &[String]) -> Option<usize> {
252 let at = columns.iter().map(|column| table.column_index(column)).collect::<Option<Vec<_>>>()?;
253 rudb_native::graph::key_of(&at)
254}
255
256/// The first table of that name in any schema of any database.
257///
258/// A relationship names a table and not a qualified name, because the `graph_links` grammar has no
259/// dot in it and the tables of one benchmark are in one schema. A name that is ambiguous across two
260/// databases resolves to the first, which is the same order `duckdb_tables()` lists them in, and
261/// the day the grammar takes a qualified name this stops guessing.
262fn table_named<'a>(catalog: &'a Catalog, name: &str) -> Option<&'a Table> {
263 catalog
264 .databases()
265 .iter()
266 .flat_map(|database| database.schemas().iter().flat_map(rudb_catalog::Schema::tables))
267 .find(|table| table.name().table.eq_ignore_ascii_case(name))
268}
269
270/// What the build observed, and why it is not more than that.
271///
272/// The note is the column that keeps this table honest. Every relationship starts unverified, and a
273/// reader who sees that wants to know whether it is unverified because the key repeats, because
274/// nothing has been built, or because the tables are not in a file yet. Those three have different
275/// answers and only one of them is a problem with the declaration.
276fn verdict(
277 catalog: &Catalog,
278 link: &Relationship,
279 stored: Option<&Stored>,
280 held: Option<&rudb_graph::Link>,
281 measured_map: bool,
282 measured_link: bool,
283) -> (&'static str, Option<&'static str>) {
284 let Some(stored) = stored else {
285 if table_named(catalog, &link.parent.table).is_none() {
286 return (Cardinality::Unverified.label(), Some("no table of that name"));
287 }
288 // A link is only written over a parent key the build found distinct, with the map it
289 // needed built for it when the file keeps none: always for a key over two columns, and for
290 // one column when the budget turned the map away. So a held link is the whole answer.
291 // Without a link there is nothing that counted the parent's keys either.
292 if held.is_some() {
293 return linked(held, measured_link);
294 }
295 if link.parent.columns.len() == 2 {
296 if measured_link {
297 return (
298 Cardinality::Unverified.label(),
299 Some("the link was measured and not kept, so link_bytes is what it would cost"),
300 );
301 }
302 return (Cardinality::Unverified.label(), Some("no link is stored"));
303 }
304 if link.parent.columns.len() != 1 {
305 return (
306 Cardinality::Unverified.label(),
307 Some("no link is built over a key this wide"),
308 );
309 }
310 // A build that looked and decided against it is a fourth answer, and the note says so
311 // without saying why, because the record keeps the size and not the reason. The two
312 // reasons a build has are the budget and a key that repeats, and key_map_bytes against
313 // graph_budget is what tells them apart.
314 if measured_map {
315 return (
316 Cardinality::Unverified.label(),
317 Some(
318 "the key map was measured and not kept, so key_map_bytes is what it would cost",
319 ),
320 );
321 }
322 return (Cardinality::Unverified.label(), Some("no key map is stored"));
323 };
324 if !stored.distinct {
325 return (
326 Cardinality::Unverified.label(),
327 Some("the parent key repeats, so this is not a many to one relationship"),
328 );
329 }
330 linked(held, measured_link)
331}
332
333/// What the link says, for a parent key the build found distinct.
334///
335/// At most one until the link is built, because exactly one is a claim about the child side and the
336/// key map only ever saw the parent's. The link is what observes the child: a link whose every child
337/// found a parent is a relationship that is total in the direction the declaration claims, and one
338/// that did not is still at most one and is why the monotone form was refused.
339fn linked(
340 held: Option<&rudb_graph::Link>,
341 measured_link: bool,
342) -> (&'static str, Option<&'static str>) {
343 match held {
344 Some(held) if held.linked() == held.children() => (Cardinality::ExactlyOne.label(), None),
345 Some(_) => (
346 Cardinality::AtMostOne.label(),
347 Some("some child rows have no parent, so this is not exactly one"),
348 ),
349 None if measured_link => (
350 Cardinality::AtMostOne.label(),
351 Some("the link was measured and not kept, so link_bytes is what it would cost"),
352 ),
353 None => (Cardinality::AtMostOne.label(), Some("no link is stored")),
354 }
355}