Skip to main content

vantage_sql/primitives/
identifier.rs

1use vantage_expressions::{Expression, Expressive};
2
3/// SQL identifier with optional qualification and alias.
4///
5/// Quoting is determined by the `Expressive<T>` impl — each backend
6/// renders with its own quote style (`"` for PostgreSQL/SQLite,
7/// `` ` `` for MySQL). This means `Identifier` is quote-agnostic;
8/// the quoting happens only when `.expr()` is called for a specific type.
9///
10/// **Warning:** Identifier names are not escaped for embedded quote characters.
11/// Do not pass untrusted user input as identifier names — this is intended
12/// for code-defined table/column names only.
13///
14/// # Examples
15///
16/// ```ignore
17/// use vantage_sql::primitives::identifier::ident;
18///
19/// // Simple column — quoting depends on which Expressive<T> is used
20/// let expr = mysql_expr!("SELECT {} FROM {}", (ident("name")), (ident("product")));
21///
22/// // Qualified (table.column)
23/// let expr = mysql_expr!("SELECT {}", (ident("name").dot_of("u")));
24///
25/// // With alias
26/// let expr = mysql_expr!("SELECT {}", (ident("name").with_alias("n")));
27/// ```
28#[derive(Debug, Clone)]
29pub struct Identifier {
30    parts: Vec<String>,
31    alias: Option<String>,
32}
33
34impl Identifier {
35    /// Single identifier: `name`.
36    pub fn new(name: impl Into<String>) -> Self {
37        Self {
38            parts: vec![name.into()],
39            alias: None,
40        }
41    }
42
43    /// Prepends a qualifier: `ident("name").dot_of("u")` → `u.name`.
44    /// Chaining adds further left: `ident("col").dot_of("t").dot_of("s")` → `s.t.col`.
45    pub fn dot_of(mut self, prefix: impl Into<String>) -> Self {
46        self.parts.insert(0, prefix.into());
47        self
48    }
49
50    /// Adds an AS alias.
51    pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
52        self.alias = Some(alias.into());
53        self
54    }
55
56    /// Render with a given quote character. Used by backend `Expressive` impls.
57    fn render_with(&self, q: char) -> String {
58        let base = self
59            .parts
60            .iter()
61            .map(|p| format!("{q}{p}{q}"))
62            .collect::<Vec<_>>()
63            .join(".");
64        match &self.alias {
65            Some(alias) => format!("{base} AS {q}{alias}{q}"),
66            None => base,
67        }
68    }
69}
70
71/// Shorthand for `Identifier::new(name)`.
72pub fn ident(name: impl Into<String>) -> Identifier {
73    Identifier::new(name)
74}
75
76// Each backend impl owns its quoting style.
77
78#[cfg(feature = "sqlite")]
79impl Expressive<crate::sqlite::types::AnySqliteType> for Identifier {
80    fn expr(&self) -> Expression<crate::sqlite::types::AnySqliteType> {
81        Expression::new(self.render_with('"'), vec![])
82    }
83}
84
85#[cfg(feature = "sqlite")]
86impl From<Identifier> for Expression<crate::sqlite::types::AnySqliteType> {
87    fn from(id: Identifier) -> Self {
88        id.expr()
89    }
90}
91
92#[cfg(feature = "postgres")]
93impl Expressive<crate::postgres::types::AnyPostgresType> for Identifier {
94    fn expr(&self) -> Expression<crate::postgres::types::AnyPostgresType> {
95        Expression::new(self.render_with('"'), vec![])
96    }
97}
98
99#[cfg(feature = "postgres")]
100impl From<Identifier> for Expression<crate::postgres::types::AnyPostgresType> {
101    fn from(id: Identifier) -> Self {
102        id.expr()
103    }
104}
105
106#[cfg(feature = "mysql")]
107impl Expressive<crate::mysql::types::AnyMysqlType> for Identifier {
108    fn expr(&self) -> Expression<crate::mysql::types::AnyMysqlType> {
109        Expression::new(self.render_with('`'), vec![])
110    }
111}
112
113#[cfg(feature = "mysql")]
114impl From<Identifier> for Expression<crate::mysql::types::AnyMysqlType> {
115    fn from(id: Identifier) -> Self {
116        id.expr()
117    }
118}