nodedb_sql/parser/
normalize.rs1use crate::error::{Result, SqlError};
6
7pub const SCHEMA_QUALIFIED_MSG: &str = "schema-qualified names are not supported; NodeDB has no schema concept \
10 — use 'users' not 'public.users'";
11
12pub fn normalize_ident(ident: &sqlparser::ast::Ident) -> String {
14 if ident.quote_style.is_some() {
15 ident.value.clone()
16 } else {
17 ident.value.to_lowercase()
18 }
19}
20
21pub fn normalize_object_name_checked(name: &sqlparser::ast::ObjectName) -> Result<String> {
27 if name.0.len() > 1 {
28 let qualified: String = name
30 .0
31 .iter()
32 .map(|part| match part {
33 sqlparser::ast::ObjectNamePart::Identifier(ident) => ident.value.clone(),
34 _ => String::new(),
35 })
36 .collect::<Vec<_>>()
37 .join(".");
38 return Err(SqlError::Unsupported {
39 detail: format!("'{qualified}': {SCHEMA_QUALIFIED_MSG}"),
40 });
41 }
42 Ok(name
43 .0
44 .first()
45 .map(|part| match part {
46 sqlparser::ast::ObjectNamePart::Identifier(ident) => normalize_ident(ident),
47 _ => String::new(),
48 })
49 .unwrap_or_default())
50}
51
52pub fn table_name_from_factor(
56 factor: &sqlparser::ast::TableFactor,
57) -> Result<Option<(String, Option<String>)>> {
58 match factor {
59 sqlparser::ast::TableFactor::Table { name, alias, .. } => {
60 let table = normalize_object_name_checked(name)?;
61 let alias_name = alias.as_ref().map(|a| normalize_ident(&a.name));
62 Ok(Some((table, alias_name)))
63 }
64 _ => Ok(None),
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use crate::parser::statement::parse_sql;
72 use sqlparser::ast::Statement;
73
74 fn parse_object_name(sql: &str) -> sqlparser::ast::ObjectName {
75 let stmts = parse_sql(sql).expect("parse failed");
77 let Statement::Query(q) = &stmts[0] else {
78 panic!("expected query");
79 };
80 let sqlparser::ast::SetExpr::Select(sel) = q.body.as_ref() else {
81 panic!("expected select body");
82 };
83 match &sel.from[0].relation {
84 sqlparser::ast::TableFactor::Table { name, .. } => name.clone(),
85 other => panic!("expected table factor, got {other:?}"),
86 }
87 }
88
89 #[test]
90 fn plain_name_accepted() {
91 let name = parse_object_name("SELECT * FROM users");
92 assert_eq!(normalize_object_name_checked(&name).unwrap(), "users");
93 }
94
95 #[test]
96 fn schema_qualified_two_parts_rejected() {
97 let name = parse_object_name("SELECT * FROM public.users");
98 let err = normalize_object_name_checked(&name).unwrap_err();
99 assert!(
100 matches!(err, SqlError::Unsupported { .. }),
101 "expected Unsupported, got {err:?}"
102 );
103 let msg = format!("{err}");
104 assert!(
105 msg.contains("public.users") || msg.contains("schema-qualified"),
106 "error should mention the qualified name or schema: {msg}"
107 );
108 }
109
110 #[test]
111 fn schema_qualified_three_parts_rejected() {
112 use sqlparser::ast::{Ident, ObjectName, ObjectNamePart};
116 let name = ObjectName(vec![
117 ObjectNamePart::Identifier(Ident::new("db")),
118 ObjectNamePart::Identifier(Ident::new("public")),
119 ObjectNamePart::Identifier(Ident::new("users")),
120 ]);
121 let err = normalize_object_name_checked(&name).unwrap_err();
122 assert!(
123 matches!(err, SqlError::Unsupported { .. }),
124 "expected Unsupported, got {err:?}"
125 );
126 }
127}