Skip to main content

sql_insight/
reference.rs

1//! Reference (identity) types shared by SQL inspection features.
2//!
3//! [`TableReference`] / [`ColumnReference`] are *qualified names* that
4//! denote a table / column in a catalog or schema — pure identity, not
5//! a relation (no tuples) nor a schema (no attribute types). They carry
6//! only enough to name the thing and compare two names for equality.
7
8use core::fmt;
9
10use crate::casing::IdentifierCasing;
11use crate::error::Error;
12use sqlparser::ast::{Ident, Insert, ObjectName, TableFactor, TableObject};
13
14/// Physical table identity — the `catalog.schema.name` triplet.
15///
16/// `TableReference` deliberately carries no alias: aliasing is a
17/// use-site decoration, not part of a table's identity. Use-site alias
18/// information, when needed, is carried by the structures that wrap a
19/// `TableReference` (e.g. resolver bindings).
20///
21/// **Equality has two levels.** The derived `Eq` / `Hash` are
22/// *structural* — case- and quote-sensitive, exact segments. That is the
23/// right dedup when references come from catalog-backed analysis (matched
24/// tables are canonicalized, so equal tables produce equal references) and
25/// for direct cross-statement comparison. For catalog-free dedup, where
26/// the same table may appear under fold-equivalent spellings (`users` vs
27/// `USERS`), use [`identity_key`](Self::identity_key) /
28/// [`same_table`](Self::same_table), which fold by a dialect's
29/// [`IdentifierCasing`].
30#[derive(Clone, Debug, PartialEq, Eq, Hash)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize))]
32pub struct TableReference {
33    #[cfg_attr(
34        feature = "serde",
35        serde(serialize_with = "crate::serde_support::opt_ident")
36    )]
37    pub catalog: Option<Ident>,
38    #[cfg_attr(
39        feature = "serde",
40        serde(serialize_with = "crate::serde_support::opt_ident")
41    )]
42    pub schema: Option<Ident>,
43    #[cfg_attr(
44        feature = "serde",
45        serde(serialize_with = "crate::serde_support::ident")
46    )]
47    pub name: Ident,
48}
49
50/// One read-side occurrence of a [`TableReference`], pairing the
51/// identity with how the resolver resolved it ([`ResolutionKind`]).
52///
53/// The table-granularity mirror of [`ColumnRead`]. Read-side surfaces
54/// ([`TableOperation::reads`] and [`TableLineageEdge::source`]) use this
55/// wrapper so each occurrence can carry resolution metadata while
56/// [`TableReference`] stays identity-only. The write-side counterpart is
57/// [`TableWrite`] ([`TableOperation::writes`], [`TableLineageEdge::target`]).
58///
59/// Unlike [`ColumnRead`], `reference` is **always present**: a table's
60/// name is written out in the SQL, so even an
61/// [`Ambiguous`](ResolutionKind::Ambiguous) table read (the catalog
62/// holds several tables matching an under-qualified name) still surfaces
63/// the reference as written. [`Unresolved`](ResolutionKind::Unresolved)
64/// therefore never arises at table granularity — it is columns-only.
65/// The resolution records how the catalog matched the table:
66/// [`Cataloged`](ResolutionKind::Cataloged) for a unique registered hit,
67/// [`Ambiguous`](ResolutionKind::Ambiguous) for several, and
68/// [`Inferred`](ResolutionKind::Inferred) for a catalog miss or
69/// catalog-less mode.
70///
71/// [`TableOperation::reads`]: crate::extractor::TableOperation::reads
72/// [`TableOperation::writes`]: crate::extractor::TableOperation::writes
73/// [`TableLineageEdge::source`]: crate::extractor::TableLineageEdge::source
74/// [`TableLineageEdge::target`]: crate::extractor::TableLineageEdge::target
75#[derive(Clone, Debug, PartialEq, Eq, Hash)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize))]
77pub struct TableRead {
78    pub reference: TableReference,
79    pub resolution: ResolutionKind,
80}
81
82/// One write-side occurrence of a [`TableReference`] — a DML / DDL write
83/// target — pairing the identity with how the catalog matched it
84/// ([`ResolutionKind`]).
85///
86/// The write-role counterpart of [`TableRead`], kept a distinct type so a
87/// read can't be passed where a write is meant (and so the write side can
88/// diverge later). The `resolution` carries the same catalog-match outcome a
89/// scanned source would: [`Cataloged`](ResolutionKind::Cataloged) for a unique
90/// registered hit, [`Ambiguous`](ResolutionKind::Ambiguous) for several, and
91/// [`Inferred`](ResolutionKind::Inferred) for a catalog miss or catalog-less
92/// mode — so the [`Cataloged`](ResolutionKind::Cataloged)-detects-catalog-aware
93/// invariant holds on writes too. `reference` is always present (a target's
94/// name is written out), so [`Unresolved`](ResolutionKind::Unresolved) never
95/// arises here, exactly as for [`TableRead`].
96///
97/// [`TableOperation::writes`]: crate::extractor::TableOperation::writes
98#[derive(Clone, Debug, PartialEq, Eq, Hash)]
99#[cfg_attr(feature = "serde", derive(serde::Serialize))]
100pub struct TableWrite {
101    pub reference: TableReference,
102    pub resolution: ResolutionKind,
103}
104
105/// A column-level identity reference: an optional owning table plus the
106/// column name.
107///
108/// `table` is `Option` because a column the resolver couldn't pin to a
109/// single owning table — [`Ambiguous`](ResolutionKind::Ambiguous) or
110/// [`Unresolved`](ResolutionKind::Unresolved) (see
111/// [`ColumnRead::resolution`] for *why*) — still surfaces its name with
112/// `table: None`. Identity is name-based: two `ColumnReference`s with the
113/// same `table` and `name` compare equal, independent of where they
114/// appeared in the SQL or how the resolver placed them. (For dialect-aware
115/// equality, see [`identity_key`](Self::identity_key).)
116#[derive(Clone, Debug, PartialEq, Eq, Hash)]
117#[cfg_attr(feature = "serde", derive(serde::Serialize))]
118pub struct ColumnReference {
119    pub table: Option<TableReference>,
120    #[cfg_attr(
121        feature = "serde",
122        serde(serialize_with = "crate::serde_support::ident")
123    )]
124    pub name: Ident,
125}
126
127/// One read-side occurrence of a [`ColumnReference`], pairing the
128/// identity with how the resolver resolved it ([`ResolutionKind`]).
129///
130/// Read-side surfaces ([`ColumnOperation::reads`] and
131/// [`ColumnLineageEdge::source`]) use this wrapper so the same column
132/// referenced twice can carry per-occurrence resolution metadata
133/// without breaking [`ColumnReference`]'s identity-only contract. The
134/// write-side counterpart is [`ColumnWrite`].
135///
136/// [`ColumnOperation::reads`]: crate::extractor::ColumnOperation::reads
137/// [`ColumnLineageEdge::source`]: crate::extractor::ColumnLineageEdge::source
138#[derive(Clone, Debug, PartialEq, Eq, Hash)]
139#[cfg_attr(feature = "serde", derive(serde::Serialize))]
140pub struct ColumnRead {
141    pub reference: ColumnReference,
142    pub resolution: ResolutionKind,
143}
144
145/// One write-side occurrence of a [`ColumnReference`] — a written column —
146/// pairing the identity with how the resolver resolved it against the target
147/// ([`ResolutionKind`]). The write-role counterpart of [`ColumnRead`], kept a
148/// distinct type so a read can't be passed where a write is meant.
149///
150/// `resolution` is the column's catalog match against its (always pinned) write
151/// target: [`Cataloged`](ResolutionKind::Cataloged) when the column is in the
152/// target's catalog column list, else [`Inferred`](ResolutionKind::Inferred)
153/// (catalog-free, the target's columns aren't known, the column isn't listed,
154/// or a freshly created / altered relation). A written column's owning table is
155/// always pinned and the column is named, so
156/// [`Unresolved`](ResolutionKind::Unresolved) /
157/// [`Ambiguous`](ResolutionKind::Ambiguous) never arise — mirroring how a base
158/// column read resolves against its relation's column list.
159///
160/// [`ColumnOperation::writes`]: crate::extractor::ColumnOperation::writes
161/// [`ColumnTarget::Relation`]: crate::extractor::ColumnTarget::Relation
162#[derive(Clone, Debug, PartialEq, Eq, Hash)]
163#[cfg_attr(feature = "serde", derive(serde::Serialize))]
164pub struct ColumnWrite {
165    pub reference: ColumnReference,
166    pub resolution: ResolutionKind,
167}
168
169/// How a reference was resolved — "what kind of resolution backs this
170/// `(table, name)` placement?".
171///
172/// Catalog-less mode runs as an *inference mode*: every real-table
173/// binding's schema is unknown, so a single-candidate resolution
174/// is best-effort, not catalog-backed. CTE and derived bodies do carry
175/// known schemas (the resolver derives them from the body's
176/// projection), but those refs are synthetic and dropped from the
177/// public reads / lineage by the resolver's post-pass.
178///
179/// `Ambiguous` and `Unresolved` are the two failure modes. Both come
180/// with `table: None` on the [`ColumnReference`]; the variant tells
181/// the consumer *why* the resolver gave up. (`Unresolved` arises only
182/// for columns — a table reference always has a name present.)
183///
184/// # Invariants
185///
186/// - **Catalog-less mode → no public `Cataloged`**: every surviving
187///   non-synthetic ref points at an unknown real table, so the
188///   strongest claim the resolver can make is
189///   [`Inferred`](Self::Inferred). Catalog-aware analysis is
190///   therefore detectable by the presence of `Cataloged`.
191/// - **Catalog-aware mode does not imply `Cataloged`**: catalogs are
192///   often partial. Refs against tables the catalog doesn't cover,
193///   or against a real unknown table that won a multi-candidate
194///   tiebreaker over known ones, both still come back as
195///   [`Inferred`](Self::Inferred).
196///
197/// # How each variant arises
198///
199/// | Situation | ResolutionKind |
200/// |---|---|
201/// | catalog-less, real unknown table, sole candidate | [`Inferred`](Self::Inferred) |
202/// | catalog-less, two real unknown tables in scope | [`Ambiguous`](Self::Ambiguous) |
203/// | catalog-less, CTE known body confirms the column | (internal `Cataloged`; synthetic, dropped) |
204/// | catalog-less, CTE known body denies the column (`SELECT typo FROM cte` where cte = `[id]`) | [`Unresolved`](Self::Unresolved) |
205/// | catalog-aware, known binding lists the column | [`Cataloged`](Self::Cataloged) |
206/// | catalog-aware, known binding *doesn't* list the column | [`Unresolved`](Self::Unresolved) |
207/// | catalog-aware, one known confirms + one unknown suspect (known-witness-over-unknown-suspects) | [`Inferred`](Self::Inferred) |
208/// | catalog-aware, two or more known schemas confirm | [`Ambiguous`](Self::Ambiguous) |
209/// | qualified `t.col` where `t` is unknown | [`Inferred`](Self::Inferred) |
210/// | qualified `t.col` where `t` is known and lists `col` | [`Cataloged`](Self::Cataloged) |
211///
212/// # Consumer guidance
213///
214/// - **Strict mode validation**: a fully resolved, catalog-confirmed
215///   statement satisfies
216///   `op.diagnostics.is_empty() && op.reads.iter().all(|r| r.resolution == ResolutionKind::Cataloged)`.
217/// - **DFD / CRUD comprehension**: treat
218///   [`Cataloged`](Self::Cataloged) and [`Inferred`](Self::Inferred)
219///   interchangeably as "resolved" (use the `(table, name)` pair);
220///   treat [`Ambiguous`](Self::Ambiguous) and
221///   [`Unresolved`](Self::Unresolved) as "incomplete".
222#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
223#[cfg_attr(feature = "serde", derive(serde::Serialize))]
224pub enum ResolutionKind {
225    /// Backed by a known schema that lists the column / names the
226    /// table. On the public surface this means a catalog (or registry)
227    /// entry backed the reference. Internally a CTE / derived body's
228    /// known schema also yields this variant on a synthetic ref, but
229    /// the post-pass drops those — so consumers only ever see
230    /// `Cataloged` for catalog-backed real references.
231    Cataloged,
232    /// Resolution succeeded by assuming the reference exists where the
233    /// resolver placed it: an unknown-schema binding adopted as the
234    /// sole candidate, a qualified reference whose qualifier alone
235    /// determined the table, or a known witness winning over
236    /// unknown suspects in a multi-candidate scope. All defensible
237    /// inferences in catalog-less or partial-catalog mode, but not
238    /// proven.
239    Inferred,
240    /// Multiple plausible candidates and the resolver couldn't pick
241    /// one: either two-or-more known schemas confirmed the column
242    /// (genuine ambiguity), or every candidate was an unknown
243    /// suspect with no tiebreaker. `ColumnReference.table` is `None`.
244    Ambiguous,
245    /// No in-scope binding could plausibly own the column: either
246    /// every known schema in scope explicitly denied it, or the
247    /// scope chain held no bindings at all. `ColumnReference.table`
248    /// is `None`. Columns only.
249    Unresolved,
250}
251
252impl TableReference {
253    pub(crate) fn try_from_name(name: &ObjectName) -> Result<Self, Error> {
254        // Every part must be a plain identifier. A non-identifier part — e.g.
255        // Snowflake's `IDENTIFIER('t')`, a function-computed name — makes the
256        // reference unrepresentable; `as_ident` is `None` there, so the
257        // all-or-nothing `collect` yields `None` and we return `Err` rather
258        // than `unwrap`-panicking (callers drop it best-effort).
259        let parts = name
260            .0
261            .iter()
262            .map(|part| part.as_ident())
263            .collect::<Option<Vec<&Ident>>>()
264            .ok_or_else(|| {
265                Error::AnalysisError(format!(
266                    "table name `{name}` is not a plain identifier path"
267                ))
268            })?;
269        match parts.as_slice() {
270            [] => Err(Error::AnalysisError(
271                "ObjectName has no identifiers".to_string(),
272            )),
273            [n] => Ok(TableReference {
274                catalog: None,
275                schema: None,
276                name: (*n).clone(),
277            }),
278            [schema, n] => Ok(TableReference {
279                catalog: None,
280                schema: Some((*schema).clone()),
281                name: (*n).clone(),
282            }),
283            [catalog, schema, n] => Ok(TableReference {
284                catalog: Some((*catalog).clone()),
285                schema: Some((*schema).clone()),
286                name: (*n).clone(),
287            }),
288            _ => Err(Error::AnalysisError(
289                "Too many identifiers provided".to_string(),
290            )),
291        }
292    }
293
294    /// Format a slice of `TableReference`s as a comma-separated string
295    /// (e.g. `"t1, schema.t2, catalog.schema.t3"`). Shared by the
296    /// table-extractor `Display` surfaces.
297    pub(crate) fn format_list(tables: &[Self]) -> String {
298        tables
299            .iter()
300            .map(|t| t.to_string())
301            .collect::<Vec<_>>()
302            .join(", ")
303    }
304
305    /// Decode an `[Ident]` slice into a `TableReference`. 1 element =
306    /// bare name, 2 = `schema.name`, 3 = `catalog.schema.name`. Returns
307    /// `None` for 0 or 4+ parts. Use [`Self::try_from_name`] when the
308    /// input is an [`ObjectName`] (4+ parts surface as `Error` there).
309    pub(crate) fn try_from_parts(parts: &[Ident]) -> Option<Self> {
310        match parts {
311            [name] => Some(TableReference {
312                catalog: None,
313                schema: None,
314                name: name.clone(),
315            }),
316            [schema, name] => Some(TableReference {
317                catalog: None,
318                schema: Some(schema.clone()),
319                name: name.clone(),
320            }),
321            [catalog, schema, name] => Some(TableReference {
322                catalog: Some(catalog.clone()),
323                schema: Some(schema.clone()),
324                name: name.clone(),
325            }),
326            _ => None,
327        }
328    }
329
330    /// Parse an INSERT statement's target into (identity, alias) pair.
331    pub(crate) fn from_insert_with_alias(value: &Insert) -> Result<(Self, Option<Ident>), Error> {
332        let name = match &value.table {
333            TableObject::TableName(object_name) => object_name,
334            TableObject::TableFunction(function) => &function.name,
335        };
336        Ok((Self::try_from_name(name)?, value.table_alias.clone()))
337    }
338
339    /// Parse a `TableFactor::Table` into (identity, alias) pair. Other
340    /// `TableFactor` variants (Derived / NestedJoin / Pivot / Unpivot /
341    /// MatchRecognize / TableFunction / Function) do not name a stored
342    /// table, so they surface as an `AnalysisError`.
343    pub(crate) fn from_table_factor_with_alias(
344        table: &TableFactor,
345    ) -> Result<(Self, Option<Ident>), Error> {
346        match table {
347            TableFactor::Table { name, alias, .. } => Ok((
348                Self::try_from_name(name)?,
349                alias.as_ref().map(|a| a.name.clone()),
350            )),
351            _ => Err(Error::AnalysisError(
352                "TableFactor variant other than Table cannot be converted to a TableReference"
353                    .to_string(),
354            )),
355        }
356    }
357}
358
359impl fmt::Display for TableReference {
360    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361        let mut parts = Vec::new();
362        if let Some(catalog) = &self.catalog {
363            parts.push(catalog.to_string());
364        }
365        if let Some(schema) = &self.schema {
366            parts.push(schema.to_string());
367        }
368        parts.push(self.name.to_string());
369        write!(f, "{}", parts.join("."))
370    }
371}
372
373impl fmt::Display for ColumnReference {
374    /// `table.column` when the owning table is known (the table renders as
375    /// its own [`TableReference`] path), otherwise just `column`. Mirrors
376    /// [`TableReference`]'s `Display` for the column-identity case.
377    ///
378    /// ```rust
379    /// use sql_insight::{ColumnReference, TableReference};
380    ///
381    /// let qualified = ColumnReference {
382    ///     table: Some(TableReference {
383    ///         catalog: None,
384    ///         schema: Some("public".into()),
385    ///         name: "users".into(),
386    ///     }),
387    ///     name: "id".into(),
388    /// };
389    /// assert_eq!(qualified.to_string(), "public.users.id");
390    ///
391    /// let bare = ColumnReference { table: None, name: "id".into() };
392    /// assert_eq!(bare.to_string(), "id");
393    /// ```
394    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
395        match &self.table {
396            Some(table) => write!(f, "{table}.{}", self.name),
397            None => write!(f, "{}", self.name),
398        }
399    }
400}
401
402/// An opaque, dialect-aware identity key for a [`TableReference`].
403///
404/// Two references whose keys are equal denote the same table *under the
405/// given dialect's case-folding* — e.g. `users` and `USERS` share a key in
406/// PostgreSQL, but not in a case-sensitive dialect. Use it to deduplicate
407/// references catalog-free, where the structural `Eq` / `Hash` on
408/// `TableReference` (case-sensitive, quote-sensitive) would over-count
409/// fold-equivalent spellings. (With a catalog, matched references are
410/// already canonicalized, so structural dedup suffices.)
411///
412/// The key is **identity**, not wildcard matching: every present segment is
413/// significant, so a bare `users` and a qualified `public.users` have
414/// *different* keys (they are different identities). The folded text is not
415/// observable — only equality / hashing.
416#[derive(Clone, Debug, PartialEq, Eq, Hash)]
417pub struct TableIdentityKey {
418    catalog: Option<String>,
419    schema: Option<String>,
420    name: String,
421}
422
423/// An opaque, dialect-aware identity key for a [`ColumnReference`] — the
424/// [`TableIdentityKey`] of its owning table (if any, folded by the table
425/// rule) plus the column name folded by the column rule. See
426/// [`TableIdentityKey`] for the identity-vs-matching and opacity notes.
427#[derive(Clone, Debug, PartialEq, Eq, Hash)]
428pub struct ColumnIdentityKey {
429    table: Option<TableIdentityKey>,
430    name: String,
431}
432
433impl TableReference {
434    /// The dialect-aware [`TableIdentityKey`] for this reference: each
435    /// segment folded by `casing`'s table rule. Equal keys denote the same
436    /// table under that dialect's casing.
437    ///
438    /// This is the **catalog-free dedup key**. The structural `Eq` on
439    /// `TableReference` is exact (case- and quote-sensitive), so without a
440    /// catalog to canonicalize spellings it over-counts `users` and `USERS`
441    /// as two tables; folding by the dialect's casing collapses them.
442    ///
443    /// ```rust
444    /// use std::collections::HashSet;
445    /// use sql_insight::{CaseRule, IdentifierCasing, TableReference};
446    ///
447    /// let users = TableReference { catalog: None, schema: None, name: "users".into() };
448    /// let upper = TableReference { catalog: None, schema: None, name: "USERS".into() };
449    ///
450    /// // Structural equality is exact — these read as two different tables.
451    /// assert_ne!(users, upper);
452    ///
453    /// // Under a case-folding dialect (here lower-folding, e.g. PostgreSQL)
454    /// // they share one identity.
455    /// let casing = IdentifierCasing::uniform(CaseRule::Lower);
456    /// assert!(users.same_table(&upper, &casing));
457    ///
458    /// // So a fold-keyed set counts the table once, where a structural
459    /// // `HashSet<TableReference>` would count two.
460    /// let distinct: HashSet<_> = [&users, &upper]
461    ///     .iter()
462    ///     .map(|t| t.identity_key(&casing))
463    ///     .collect();
464    /// assert_eq!(distinct.len(), 1);
465    ///
466    /// // Identity, not wildcard: a bare name and a schema-qualified one stay
467    /// // distinct (different identities, not a prefix match).
468    /// let qualified = TableReference {
469    ///     catalog: None,
470    ///     schema: Some("public".into()),
471    ///     name: "users".into(),
472    /// };
473    /// assert!(!users.same_table(&qualified, &casing));
474    /// ```
475    pub fn identity_key(&self, casing: &IdentifierCasing) -> TableIdentityKey {
476        let fold = |ident: &Ident| casing.table.normalize(ident);
477        TableIdentityKey {
478            catalog: self.catalog.as_ref().map(&fold),
479            schema: self.schema.as_ref().map(&fold),
480            name: fold(&self.name),
481        }
482    }
483
484    /// Whether `self` and `other` denote the same table under `casing` —
485    /// equivalent to comparing their [`identity_key`](Self::identity_key)s.
486    pub fn same_table(&self, other: &Self, casing: &IdentifierCasing) -> bool {
487        self.identity_key(casing) == other.identity_key(casing)
488    }
489}
490
491impl ColumnReference {
492    /// The dialect-aware [`ColumnIdentityKey`] for this reference: the
493    /// owning table folded by the table rule, the column name by the column
494    /// rule. Equal keys denote the same column under that dialect's casing.
495    ///
496    /// Like [`TableReference::identity_key`] this is the catalog-free dedup
497    /// key — folding both the owning table and the column name (by their
498    /// separate rules, which a dialect can set apart). The owning table is
499    /// part of the identity: same column name, different table → different
500    /// column.
501    ///
502    /// ```rust
503    /// use sql_insight::{CaseRule, ColumnReference, IdentifierCasing, TableReference};
504    ///
505    /// let owned = |t: &str| Some(TableReference { catalog: None, schema: None, name: t.into() });
506    /// let lower = ColumnReference { table: owned("users"), name: "id".into() };
507    /// let upper = ColumnReference { table: owned("USERS"), name: "ID".into() };
508    ///
509    /// // Structural equality is exact; a case-folding casing merges them.
510    /// assert_ne!(lower, upper);
511    /// let casing = IdentifierCasing::uniform(CaseRule::Insensitive);
512    /// assert!(lower.same_column(&upper, &casing));
513    ///
514    /// // A different owning table is a different column, same name or not.
515    /// let other = ColumnReference { table: owned("accounts"), name: "id".into() };
516    /// assert!(!lower.same_column(&other, &casing));
517    /// ```
518    pub fn identity_key(&self, casing: &IdentifierCasing) -> ColumnIdentityKey {
519        ColumnIdentityKey {
520            table: self.table.as_ref().map(|t| t.identity_key(casing)),
521            name: casing.column.normalize(&self.name),
522        }
523    }
524
525    /// Whether `self` and `other` denote the same column under `casing` —
526    /// equivalent to comparing their [`identity_key`](Self::identity_key)s.
527    pub fn same_column(&self, other: &Self, casing: &IdentifierCasing) -> bool {
528        self.identity_key(casing) == other.identity_key(casing)
529    }
530}
531
532impl TryFrom<&Insert> for TableReference {
533    type Error = Error;
534
535    fn try_from(value: &Insert) -> Result<Self, Self::Error> {
536        Self::from_insert_with_alias(value).map(|(table, _)| table)
537    }
538}
539
540impl TryFrom<&TableFactor> for TableReference {
541    type Error = Error;
542
543    fn try_from(table: &TableFactor) -> Result<Self, Self::Error> {
544        Self::from_table_factor_with_alias(table).map(|(table, _)| table)
545    }
546}
547
548impl TryFrom<&ObjectName> for TableReference {
549    type Error = Error;
550
551    fn try_from(obj_name: &ObjectName) -> Result<Self, Self::Error> {
552        Self::try_from_name(obj_name)
553    }
554}
555
556#[cfg(test)]
557mod tests {
558    use super::*;
559    use sqlparser::ast::{SetExpr, Statement};
560    use sqlparser::dialect::GenericDialect;
561    use sqlparser::parser::Parser;
562
563    /// The first FROM factor of `SELECT 1 FROM <from>` — a handle on a parsed
564    /// `TableFactor` (and, for a `Table`, its `ObjectName`) to drive the public
565    /// `TryFrom` conversions.
566    fn first_table_factor(from: &str) -> TableFactor {
567        let sql = format!("SELECT 1 FROM {from}");
568        let mut stmts = Parser::parse_sql(&GenericDialect {}, &sql).unwrap();
569        let Statement::Query(query) = stmts.remove(0) else {
570            panic!("expected a query");
571        };
572        let SetExpr::Select(select) = *query.body else {
573            panic!("expected a SELECT");
574        };
575        select.from.into_iter().next().unwrap().relation
576    }
577
578    #[test]
579    fn try_from_object_name_keeps_catalog_schema_name_and_displays_all_parts() {
580        let factor = first_table_factor("cat.sch.tbl");
581        let TableFactor::Table { name, .. } = &factor else {
582            panic!("expected a table factor");
583        };
584        let reference = TableReference::try_from(name).unwrap();
585        assert_eq!(reference.catalog.as_ref().unwrap().value, "cat");
586        assert_eq!(reference.schema.as_ref().unwrap().value, "sch");
587        assert_eq!(reference.name.value, "tbl");
588        // Display renders every present part (the three-part / catalog branch).
589        assert_eq!(reference.to_string(), "cat.sch.tbl");
590    }
591
592    #[test]
593    fn try_from_table_factor_converts_a_table_and_rejects_a_derived_factor() {
594        let table = first_table_factor("a.b");
595        let reference = TableReference::try_from(&table).unwrap();
596        assert_eq!(reference.schema.as_ref().unwrap().value, "a");
597        assert_eq!(reference.name.value, "b");
598        // A non-`Table` factor names no stored table — an analysis error.
599        let derived = first_table_factor("(SELECT 1) AS d");
600        assert!(matches!(
601            TableReference::try_from(&derived),
602            Err(Error::AnalysisError(_))
603        ));
604    }
605
606    #[test]
607    fn try_from_insert_takes_the_target_name() {
608        let mut stmts =
609            Parser::parse_sql(&GenericDialect {}, "INSERT INTO a.b VALUES (1)").unwrap();
610        let Statement::Insert(insert) = stmts.remove(0) else {
611            panic!("expected an insert");
612        };
613        let reference = TableReference::try_from(&insert).unwrap();
614        assert_eq!(reference.schema.as_ref().unwrap().value, "a");
615        assert_eq!(reference.name.value, "b");
616    }
617}