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
52fn normalize_table_name(name: &sqlparser::ast::ObjectName) -> Result<String> {
62 if name.0.len() == 2 {
63 let parts: Vec<String> = name
64 .0
65 .iter()
66 .map(|p| match p {
67 sqlparser::ast::ObjectNamePart::Identifier(ident) => normalize_ident(ident),
68 _ => String::new(),
69 })
70 .collect();
71 match parts[0].as_str() {
72 "pg_catalog" => return Ok(parts[1].clone()),
73 "_system" => return Ok(format!("_system.{}", parts[1])),
74 _ => {}
75 }
76 }
77 normalize_object_name_checked(name)
78}
79
80pub fn table_name_from_factor(
84 factor: &sqlparser::ast::TableFactor,
85) -> Result<Option<(String, Option<String>)>> {
86 match factor {
87 sqlparser::ast::TableFactor::Table { name, alias, .. } => {
88 let table = normalize_table_name(name)?;
89 let alias_name = alias.as_ref().map(|a| normalize_ident(&a.name));
90 Ok(Some((table, alias_name)))
91 }
92 _ => Ok(None),
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use crate::parser::statement::parse_sql;
100 use sqlparser::ast::Statement;
101
102 fn parse_object_name(sql: &str) -> sqlparser::ast::ObjectName {
103 let stmts = parse_sql(sql).expect("parse failed");
105 let Statement::Query(q) = &stmts[0] else {
106 panic!("expected query");
107 };
108 let sqlparser::ast::SetExpr::Select(sel) = q.body.as_ref() else {
109 panic!("expected select body");
110 };
111 match &sel.from[0].relation {
112 sqlparser::ast::TableFactor::Table { name, .. } => name.clone(),
113 other => panic!("expected table factor, got {other:?}"),
114 }
115 }
116
117 #[test]
118 fn plain_name_accepted() {
119 let name = parse_object_name("SELECT * FROM users");
120 assert_eq!(normalize_object_name_checked(&name).unwrap(), "users");
121 }
122
123 #[test]
124 fn schema_qualified_two_parts_rejected() {
125 let name = parse_object_name("SELECT * FROM public.users");
126 let err = normalize_object_name_checked(&name).unwrap_err();
127 assert!(
128 matches!(err, SqlError::Unsupported { .. }),
129 "expected Unsupported, got {err:?}"
130 );
131 let msg = format!("{err}");
132 assert!(
133 msg.contains("public.users") || msg.contains("schema-qualified"),
134 "error should mention the qualified name or schema: {msg}"
135 );
136 }
137
138 #[test]
139 fn schema_qualified_three_parts_rejected() {
140 use sqlparser::ast::{Ident, ObjectName, ObjectNamePart};
144 let name = ObjectName(vec![
145 ObjectNamePart::Identifier(Ident::new("db")),
146 ObjectNamePart::Identifier(Ident::new("public")),
147 ObjectNamePart::Identifier(Ident::new("users")),
148 ]);
149 let err = normalize_object_name_checked(&name).unwrap_err();
150 assert!(
151 matches!(err, SqlError::Unsupported { .. }),
152 "expected Unsupported, got {err:?}"
153 );
154 }
155}