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/// Embedded quote characters ARE escaped (doubled) when rendering, so an
11/// identifier built from a runtime value cannot break out of its quotes.
12/// It is still a NAME, not a value: prefer binding values as parameters
13/// (`expr("… = {}", [v])`) and reserve identifiers for schema elements.
14///
15/// # Examples
16///
17/// ```ignore
18/// use vantage_sql::primitives::identifier::ident;
19///
20/// // Simple column — quoting depends on which Expressive<T> is used
21/// let expr = mysql_expr!("SELECT {} FROM {}", (ident("name")), (ident("product")));
22///
23/// // Qualified (table.column)
24/// let expr = mysql_expr!("SELECT {}", (ident("name").dot_of("u")));
25///
26/// // With alias
27/// let expr = mysql_expr!("SELECT {}", (ident("name").with_alias("n")));
28/// ```
29#[derive(Debug, Clone)]
30pub struct Identifier {
31    parts: Vec<String>,
32    alias: Option<String>,
33}
34
35impl Identifier {
36    /// Single identifier: `name`.
37    pub fn new(name: impl Into<String>) -> Self {
38        Self {
39            parts: vec![name.into()],
40            alias: None,
41        }
42    }
43
44    /// Prepends a qualifier: `ident("name").dot_of("u")` → `u.name`.
45    /// Chaining adds further left: `ident("col").dot_of("t").dot_of("s")` → `s.t.col`.
46    pub fn dot_of(mut self, prefix: impl Into<String>) -> Self {
47        self.parts.insert(0, prefix.into());
48        self
49    }
50
51    /// Adds an AS alias.
52    pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
53        self.alias = Some(alias.into());
54        self
55    }
56
57    /// Returns the identifier name (parts joined with dots, no quotes).
58    pub fn name(&self) -> String {
59        self.parts.join(".")
60    }
61
62    /// Returns the alias, if any.
63    pub fn alias(&self) -> Option<&str> {
64        self.alias.as_deref()
65    }
66
67    /// Every quote character inside a part is doubled (`"a""b"`, `` `a``b` ``):
68    /// vista scripts pass runtime values to `ident(args.col)`, and an
69    /// unescaped quote would end the identifier and let the rest parse as SQL.
70    fn render_with(&self, q: char) -> String {
71        let quote = |p: &str| format!("{q}{}{q}", p.replace(q, &format!("{q}{q}")));
72        let base = self
73            .parts
74            .iter()
75            .map(|p| quote(p))
76            .collect::<Vec<_>>()
77            .join(".");
78        match &self.alias {
79            Some(alias) => format!("{base} AS {}", quote(alias)),
80            None => base,
81        }
82    }
83}
84
85/// Shorthand for `Identifier::new(name)`.
86pub fn ident(name: impl Into<String>) -> Identifier {
87    Identifier::new(name)
88}
89
90// Each backend impl owns its quoting style.
91
92#[cfg(feature = "sqlite")]
93impl Expressive<crate::sqlite::types::AnySqliteType> for Identifier {
94    fn expr(&self) -> Expression<crate::sqlite::types::AnySqliteType> {
95        Expression::new(self.render_with('"'), vec![])
96    }
97}
98
99#[cfg(feature = "sqlite")]
100impl From<Identifier> for Expression<crate::sqlite::types::AnySqliteType> {
101    fn from(id: Identifier) -> Self {
102        id.expr()
103    }
104}
105
106#[cfg(feature = "postgres")]
107impl Expressive<crate::postgres::types::AnyPostgresType> for Identifier {
108    fn expr(&self) -> Expression<crate::postgres::types::AnyPostgresType> {
109        Expression::new(self.render_with('"'), vec![])
110    }
111}
112
113#[cfg(feature = "postgres")]
114impl From<Identifier> for Expression<crate::postgres::types::AnyPostgresType> {
115    fn from(id: Identifier) -> Self {
116        id.expr()
117    }
118}
119
120#[cfg(feature = "mysql")]
121impl Expressive<crate::mysql::types::AnyMysqlType> for Identifier {
122    fn expr(&self) -> Expression<crate::mysql::types::AnyMysqlType> {
123        Expression::new(self.render_with('`'), vec![])
124    }
125}
126
127#[cfg(feature = "mysql")]
128impl From<Identifier> for Expression<crate::mysql::types::AnyMysqlType> {
129    fn from(id: Identifier) -> Self {
130        id.expr()
131    }
132}