vantage_sql/primitives/
identifier.rs1use vantage_expressions::{Expression, Expressive};
2
3#[derive(Debug, Clone)]
30pub struct Identifier {
31 parts: Vec<String>,
32 alias: Option<String>,
33}
34
35impl Identifier {
36 pub fn new(name: impl Into<String>) -> Self {
38 Self {
39 parts: vec![name.into()],
40 alias: None,
41 }
42 }
43
44 pub fn dot_of(mut self, prefix: impl Into<String>) -> Self {
47 self.parts.insert(0, prefix.into());
48 self
49 }
50
51 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
53 self.alias = Some(alias.into());
54 self
55 }
56
57 pub fn name(&self) -> String {
59 self.parts.join(".")
60 }
61
62 pub fn alias(&self) -> Option<&str> {
64 self.alias.as_deref()
65 }
66
67 fn render_with(&self, q: char) -> String {
77 let quote = |p: &str| format!("{q}{}{q}", p.replace(q, &format!("{q}{q}")));
78 let base = self
79 .parts
80 .iter()
81 .map(|p| quote(p))
82 .collect::<Vec<_>>()
83 .join(".");
84 match &self.alias {
85 Some(alias) => format!("{base} AS {}", quote(alias)),
86 None => base,
87 }
88 }
89}
90
91pub fn ident(name: impl Into<String>) -> Identifier {
93 Identifier::new(name)
94}
95
96#[cfg(feature = "sqlite")]
99impl Expressive<crate::sqlite::types::AnySqliteType> for Identifier {
100 fn expr(&self) -> Expression<crate::sqlite::types::AnySqliteType> {
101 Expression::new(self.render_with('"'), vec![])
102 }
103}
104
105#[cfg(feature = "sqlite")]
106impl From<Identifier> for Expression<crate::sqlite::types::AnySqliteType> {
107 fn from(id: Identifier) -> Self {
108 id.expr()
109 }
110}
111
112#[cfg(feature = "postgres")]
113impl Expressive<crate::postgres::types::AnyPostgresType> for Identifier {
114 fn expr(&self) -> Expression<crate::postgres::types::AnyPostgresType> {
115 Expression::new(self.render_with('"'), vec![])
116 }
117}
118
119#[cfg(feature = "postgres")]
120impl From<Identifier> for Expression<crate::postgres::types::AnyPostgresType> {
121 fn from(id: Identifier) -> Self {
122 id.expr()
123 }
124}
125
126#[cfg(feature = "mysql")]
127impl Expressive<crate::mysql::types::AnyMysqlType> for Identifier {
128 fn expr(&self) -> Expression<crate::mysql::types::AnyMysqlType> {
129 Expression::new(self.render_with('`'), vec![])
130 }
131}
132
133#[cfg(feature = "mysql")]
134impl From<Identifier> for Expression<crate::mysql::types::AnyMysqlType> {
135 fn from(id: Identifier) -> Self {
136 id.expr()
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn parts_and_alias_quote_normally() {
146 let id = ident("name").dot_of("u").with_alias("n");
147 assert_eq!(id.render_with('"'), r#""u"."name" AS "n""#);
148 }
149
150 #[test]
151 fn embedded_quotes_are_doubled_not_escaped_out_of() {
152 let id = ident(r#"x" ; DROP TABLE users --"#);
155 assert_eq!(id.render_with('"'), r#""x"" ; DROP TABLE users --""#);
156 let id = ident("a`b");
157 assert_eq!(id.render_with('`'), "`a``b`");
158 }
159
160 #[test]
161 fn alias_is_escaped_too() {
162 let id = ident("col").with_alias(r#"a"b"#);
163 assert_eq!(id.render_with('"'), r#""col" AS "a""b""#);
164 }
165}