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}