Skip to main content

dbmcp_sql_lineage/
model.rs

1//! Public lineage result types.
2//!
3//! [`TableRef`] and [`SourceRef`] compare and order case-insensitively (ASCII)
4//! so set membership matches the catalog's case-insensitive lookup semantics.
5
6use std::cmp::Ordering;
7use std::collections::BTreeSet;
8
9use serde::{Deserialize, Serialize};
10
11/// Lowercases an ASCII string for case-insensitive comparison.
12fn fold(s: &str) -> String {
13    s.to_ascii_lowercase()
14}
15
16/// A physical table, optionally schema- and database-qualified.
17///
18/// Equality, ordering, and hashing are case-insensitive (ASCII) on all parts.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct TableRef {
21    /// Optional database/catalog qualifier present in the query.
22    pub database: Option<String>,
23    /// Optional schema qualifier present in the query.
24    pub schema: Option<String>,
25    /// Physical table name, as written in the query (original case preserved).
26    pub name: String,
27}
28
29impl TableRef {
30    /// Creates a bare (unqualified) table reference.
31    pub fn new(name: impl Into<String>) -> Self {
32        Self {
33            database: None,
34            schema: None,
35            name: name.into(),
36        }
37    }
38
39    /// Creates a schema-qualified table reference (no database qualifier).
40    pub fn qualified(schema: impl Into<String>, name: impl Into<String>) -> Self {
41        Self {
42            database: None,
43            schema: Some(schema.into()),
44            name: name.into(),
45        }
46    }
47
48    /// Starts building a table reference with optional qualifiers.
49    pub fn builder(name: impl Into<String>) -> TableRefBuilder {
50        TableRefBuilder {
51            database: None,
52            schema: None,
53            name: name.into(),
54        }
55    }
56
57    /// Returns the case-insensitive comparison key `(database, schema, name)`.
58    fn key(&self) -> (Option<String>, Option<String>, String) {
59        (
60            self.database.as_deref().map(fold),
61            self.schema.as_deref().map(fold),
62            fold(&self.name),
63        )
64    }
65}
66
67/// Fluent builder for a schema/database-qualified [`TableRef`].
68#[derive(Debug, Clone)]
69pub struct TableRefBuilder {
70    database: Option<String>,
71    schema: Option<String>,
72    name: String,
73}
74
75impl TableRefBuilder {
76    /// Sets the schema qualifier.
77    #[must_use]
78    pub fn with_schema(mut self, schema: impl Into<String>) -> Self {
79        self.schema = Some(schema.into());
80        self
81    }
82
83    /// Sets the database/catalog qualifier.
84    #[must_use]
85    pub fn with_database(mut self, database: impl Into<String>) -> Self {
86        self.database = Some(database.into());
87        self
88    }
89
90    /// Builds the [`TableRef`].
91    #[must_use]
92    pub fn build(self) -> TableRef {
93        TableRef {
94            database: self.database,
95            schema: self.schema,
96            name: self.name,
97        }
98    }
99}
100
101impl PartialEq for TableRef {
102    fn eq(&self, other: &Self) -> bool {
103        self.key() == other.key()
104    }
105}
106
107impl Eq for TableRef {}
108
109impl PartialOrd for TableRef {
110    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
111        Some(self.cmp(other))
112    }
113}
114
115impl Ord for TableRef {
116    fn cmp(&self, other: &Self) -> Ordering {
117        self.key().cmp(&other.key())
118    }
119}
120
121impl std::hash::Hash for TableRef {
122    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
123        self.key().hash(state);
124    }
125}
126
127/// A concrete physical origin of (part of) an output column.
128///
129/// Equality, ordering, and hashing are case-insensitive (ASCII).
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct SourceRef {
132    /// The physical table (alias stripped, qualifier preserved).
133    pub table: TableRef,
134    /// The physical column name on that table.
135    pub column: String,
136}
137
138impl SourceRef {
139    /// Creates a source reference.
140    pub fn new(table: TableRef, column: impl Into<String>) -> Self {
141        Self {
142            table,
143            column: column.into(),
144        }
145    }
146
147    /// Returns the case-insensitive comparison key.
148    fn key(&self) -> (Option<String>, Option<String>, String, String) {
149        let (db, schema, table) = self.table.key();
150        (db, schema, table, fold(&self.column))
151    }
152}
153
154impl PartialEq for SourceRef {
155    fn eq(&self, other: &Self) -> bool {
156        self.key() == other.key()
157    }
158}
159
160impl Eq for SourceRef {}
161
162impl PartialOrd for SourceRef {
163    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
164        Some(self.cmp(other))
165    }
166}
167
168impl Ord for SourceRef {
169    fn cmp(&self, other: &Self) -> Ordering {
170        self.key().cmp(&other.key())
171    }
172}
173
174impl std::hash::Hash for SourceRef {
175    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
176        self.key().hash(state);
177    }
178}
179
180/// One column in a query's result set.
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
182pub struct OutputColumn {
183    /// Output name/alias as seen by the client; `None` for unnamed expressions.
184    pub name: Option<String>,
185    /// 0-based position in the result set.
186    pub position: usize,
187    /// Contributing source columns; empty for literals/constants.
188    pub sources: BTreeSet<SourceRef>,
189}
190
191/// The lineage extracted from a single statement.
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193pub struct Lineage {
194    /// One entry per output column, in projection (or target) order.
195    pub columns: Vec<OutputColumn>,
196    /// All physical input/source tables referenced by the statement.
197    pub tables: BTreeSet<TableRef>,
198    /// Target table for a write statement; `None` for a read (`SELECT`).
199    pub target: Option<TableRef>,
200}
201
202impl Lineage {
203    /// Returns an empty lineage (no columns, no tables, no target).
204    #[must_use]
205    pub fn empty() -> Self {
206        Self {
207            columns: Vec::new(),
208            tables: BTreeSet::new(),
209            target: None,
210        }
211    }
212
213    /// Builds a read lineage from output columns, deriving the source tables.
214    #[must_use]
215    pub(crate) fn from_columns(columns: Vec<OutputColumn>) -> Self {
216        Self {
217            tables: tables_of(&columns),
218            columns,
219            target: None,
220        }
221    }
222
223    /// Builds a write lineage for `target` from its mapped output columns.
224    ///
225    /// `tables` accumulates `input_tables` plus every source table feeding a
226    /// column; the `target` sink is intentionally excluded.
227    #[must_use]
228    pub(crate) fn from_write(target: TableRef, columns: Vec<OutputColumn>, input_tables: BTreeSet<TableRef>) -> Self {
229        let mut tables = input_tables;
230        tables.extend(tables_of(&columns));
231        Self {
232            columns,
233            tables,
234            target: Some(target),
235        }
236    }
237}
238
239/// Collects the distinct source tables feeding a set of output columns.
240fn tables_of(columns: &[OutputColumn]) -> BTreeSet<TableRef> {
241    columns
242        .iter()
243        .flat_map(|c| c.sources.iter().map(|s| s.table.clone()))
244        .collect()
245}