1use crate::error::{Result, SqlError};
6use crate::reserved::{check_ast_identifier, check_identifier};
7
8pub const SCHEMA_QUALIFIED_MSG: &str = "schema-qualified names are not supported; NodeDB has no schema concept \
11 — use 'users' not 'public.users'";
12
13pub fn normalize_ident(ident: &sqlparser::ast::Ident) -> String {
15 if ident.quote_style.is_some() {
16 ident.value.clone()
17 } else {
18 ident.value.to_lowercase()
19 }
20}
21
22pub fn normalize_object_name_checked(name: &sqlparser::ast::ObjectName) -> Result<String> {
28 if name.0.len() > 1 {
29 let qualified = name
32 .0
33 .iter()
34 .map(|part| match part {
35 sqlparser::ast::ObjectNamePart::Identifier(ident) => check_ast_identifier(ident),
36 _ => Err(SqlError::InvalidIdentifier {
37 name: String::new(),
38 reason: "object name must contain only identifiers",
39 }),
40 })
41 .collect::<Result<Vec<_>>>()?
42 .join(".");
43 return Err(SqlError::Unsupported {
44 detail: format!("'{qualified}': {SCHEMA_QUALIFIED_MSG}"),
45 });
46 }
47 let part = name.0.first().ok_or_else(|| SqlError::InvalidIdentifier {
48 name: String::new(),
49 reason: "object name must contain an identifier",
50 })?;
51 match part {
52 sqlparser::ast::ObjectNamePart::Identifier(ident) => check_ast_identifier(ident),
53 _ => Err(SqlError::InvalidIdentifier {
54 name: String::new(),
55 reason: "object name must contain an identifier",
56 }),
57 }
58}
59
60pub fn normalize_insert_column(name: &sqlparser::ast::ObjectName) -> Result<String> {
68 if let [sqlparser::ast::ObjectNamePart::Identifier(ident)] = name.0.as_slice() {
69 return Ok(normalize_ident(ident));
70 }
71 if name.0.len() == 1 {
72 return Err(SqlError::InvalidIdentifier {
74 name: String::new(),
75 reason: "an INSERT column list takes plain column names",
76 });
77 }
78 normalize_object_name_checked(name)
82}
83
84fn normalize_table_name(name: &sqlparser::ast::ObjectName) -> Result<String> {
94 if name.0.len() == 2 {
95 let parts: Vec<String> = name
96 .0
97 .iter()
98 .map(|part| match part {
99 sqlparser::ast::ObjectNamePart::Identifier(ident) => check_ast_identifier(ident),
100 _ => Err(SqlError::InvalidIdentifier {
101 name: String::new(),
102 reason: "object name must contain an identifier",
103 }),
104 })
105 .collect::<Result<_>>()?;
106 match parts[0].as_str() {
107 "pg_catalog" => return Ok(parts[1].clone()),
108 "_system" => return Ok(format!("_system.{}", parts[1])),
109 _ => {}
110 }
111 }
112 normalize_object_name_checked(name)
113}
114
115pub fn check_relation_token(raw_name: &str) -> Result<String> {
129 if raw_name.contains('"') || !raw_name.contains('.') {
130 return check_identifier(raw_name);
131 }
132 let parts = raw_name
135 .split('.')
136 .map(check_identifier)
137 .collect::<Result<Vec<_>>>()?;
138 if parts.len() == 2 {
139 match parts[0].as_str() {
140 "pg_catalog" => return Ok(parts[1].clone()),
141 "_system" => return Ok(format!("_system.{}", parts[1])),
142 _ => {}
143 }
144 }
145 Err(SqlError::Unsupported {
146 detail: format!("'{}': {SCHEMA_QUALIFIED_MSG}", parts.join(".")),
147 })
148}
149
150pub fn table_name_from_factor(
154 factor: &sqlparser::ast::TableFactor,
155) -> Result<Option<(String, Option<String>)>> {
156 match factor {
157 sqlparser::ast::TableFactor::Table { name, alias, .. } => {
158 let table = normalize_table_name(name)?;
159 let alias_name = alias
160 .as_ref()
161 .map(|alias| check_ast_identifier(&alias.name))
162 .transpose()?;
163 Ok(Some((table, alias_name)))
164 }
165 _ => Ok(None),
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use crate::parser::statement::parse_sql;
173 use sqlparser::ast::Statement;
174
175 fn parse_table_factor(sql: &str) -> sqlparser::ast::TableFactor {
176 let stmts = parse_sql(sql).expect("parse failed");
177 let Statement::Query(q) = &stmts[0] else {
178 panic!("expected query");
179 };
180 let sqlparser::ast::SetExpr::Select(sel) = q.body.as_ref() else {
181 panic!("expected select body");
182 };
183 sel.from[0].relation.clone()
184 }
185
186 fn parse_object_name(sql: &str) -> sqlparser::ast::ObjectName {
187 match parse_table_factor(sql) {
188 sqlparser::ast::TableFactor::Table { name, .. } => name,
189 other => panic!("expected table factor, got {other:?}"),
190 }
191 }
192
193 #[test]
194 fn plain_name_accepted() {
195 let name = parse_object_name("SELECT * FROM users");
196 assert_eq!(normalize_object_name_checked(&name).unwrap(), "users");
197 }
198
199 #[test]
200 fn schema_qualified_two_parts_rejected() {
201 let name = parse_object_name("SELECT * FROM public.users");
202 let err = normalize_object_name_checked(&name).unwrap_err();
203 assert!(
204 matches!(err, SqlError::Unsupported { .. }),
205 "expected Unsupported, got {err:?}"
206 );
207 let msg = format!("{err}");
208 assert!(
209 msg.contains("public.users") || msg.contains("schema-qualified"),
210 "error should mention the qualified name or schema: {msg}"
211 );
212 }
213
214 #[test]
215 fn schema_qualified_three_parts_rejected() {
216 use sqlparser::ast::{Ident, ObjectName, ObjectNamePart};
220 let name = ObjectName(vec![
221 ObjectNamePart::Identifier(Ident::new("db")),
222 ObjectNamePart::Identifier(Ident::new("public")),
223 ObjectNamePart::Identifier(Ident::new("users")),
224 ]);
225 let err = normalize_object_name_checked(&name).unwrap_err();
226 assert!(
227 matches!(err, SqlError::Unsupported { .. }),
228 "expected Unsupported, got {err:?}"
229 );
230 }
231
232 #[test]
233 fn object_names_enforce_ast_identifier_rules() {
234 use sqlparser::ast::{Ident, ObjectName, ObjectNamePart};
235
236 let empty = ObjectName(vec![ObjectNamePart::Identifier(Ident::new(""))]);
237 assert!(matches!(
238 normalize_object_name_checked(&empty),
239 Err(SqlError::InvalidIdentifier { .. })
240 ));
241
242 let reserved = ObjectName(vec![ObjectNamePart::Identifier(Ident::new("MATCH"))]);
243 assert!(matches!(
244 normalize_object_name_checked(&reserved),
245 Err(SqlError::ReservedIdentifier { .. })
246 ));
247
248 let mut quoted = Ident::new("MiXeD 雪");
249 quoted.quote_style = Some('"');
250 let quoted_name = ObjectName(vec![ObjectNamePart::Identifier(quoted)]);
251 assert_eq!(
252 normalize_object_name_checked("ed_name).expect("quoted object name"),
253 "MiXeD 雪"
254 );
255
256 let quote = ObjectName(vec![ObjectNamePart::Identifier(Ident::new("a\"b"))]);
257 assert!(matches!(
258 normalize_object_name_checked("e),
259 Err(SqlError::InvalidIdentifier { .. })
260 ));
261 }
262
263 #[test]
264 fn insert_columns_take_plain_names_only() {
265 use sqlparser::ast::{Ident, ObjectName, ObjectNamePart};
266
267 let plain = ObjectName(vec![ObjectNamePart::Identifier(Ident::new("MiXeD"))]);
268 assert_eq!(
269 normalize_insert_column(&plain).expect("plain column"),
270 "mixed"
271 );
272
273 let mut quoted_ident = Ident::new("MiXeD");
274 quoted_ident.quote_style = Some('"');
275 let quoted = ObjectName(vec![ObjectNamePart::Identifier(quoted_ident)]);
276 assert_eq!(
277 normalize_insert_column("ed).expect("quoted column"),
278 "MiXeD"
279 );
280
281 let qualified = ObjectName(vec![
285 ObjectNamePart::Identifier(Ident::new("a")),
286 ObjectNamePart::Identifier(Ident::new("b")),
287 ]);
288 assert!(matches!(
289 normalize_insert_column(&qualified),
290 Err(SqlError::Unsupported { .. })
291 ));
292
293 let empty = ObjectName(Vec::new());
294 assert!(matches!(
295 normalize_insert_column(&empty),
296 Err(SqlError::InvalidIdentifier { .. })
297 ));
298 }
299
300 #[test]
301 fn relation_tokens_accept_the_two_system_qualifiers() {
302 assert_eq!(
303 check_relation_token("_system.audit_log").expect("system relation"),
304 "_system.audit_log"
305 );
306 assert_eq!(
307 check_relation_token("pg_catalog.pg_class").expect("catalog relation"),
308 "pg_class"
309 );
310 assert_eq!(check_relation_token("MiXeD").expect("bare"), "mixed");
311 assert_eq!(check_relation_token("\"MiXeD\"").expect("quoted"), "MiXeD");
312 }
313
314 #[test]
315 fn relation_tokens_reject_other_qualifiers() {
316 for token in ["public.users", "db.public.users"] {
317 assert!(
318 matches!(
319 check_relation_token(token),
320 Err(SqlError::Unsupported { .. })
321 ),
322 "{token}"
323 );
324 }
325 assert!(matches!(
329 check_relation_token("_system.\"a.b\""),
330 Err(SqlError::InvalidIdentifier { .. })
331 ));
332 }
333
334 #[test]
335 fn qualified_names_validate_components_before_rejection() {
336 use sqlparser::ast::{Ident, ObjectName, ObjectNamePart};
337
338 for invalid_value in ["bad\nname", "a\"b"] {
339 let mut invalid = Ident::new(invalid_value);
340 invalid.quote_style = Some('"');
341 let name = ObjectName(vec![
342 ObjectNamePart::Identifier(Ident::new("public")),
343 ObjectNamePart::Identifier(invalid),
344 ]);
345 assert!(matches!(
346 normalize_object_name_checked(&name),
347 Err(SqlError::InvalidIdentifier { .. })
348 ));
349 }
350 }
351
352 #[test]
353 fn table_aliases_enforce_ast_identifier_rules() {
354 use crate::planner::lateral::subquery::lateral_alias_from_factor;
355
356 let quoted = parse_table_factor("SELECT * FROM users AS \"MiXeD 雪\"");
357 assert_eq!(
358 table_name_from_factor("ed).expect("valid alias"),
359 Some(("users".to_string(), Some("MiXeD 雪".to_string())))
360 );
361
362 for alias in ["\"\"", "\"a\"\"b\""] {
363 let ordinary = parse_table_factor(&format!("SELECT * FROM users AS {alias}"));
364 assert!(matches!(
365 table_name_from_factor(&ordinary),
366 Err(SqlError::InvalidIdentifier { .. })
367 ));
368
369 let lateral =
370 parse_table_factor(&format!("SELECT * FROM LATERAL (SELECT 1) AS {alias}"));
371 assert!(matches!(
372 lateral_alias_from_factor(&lateral),
373 Err(SqlError::InvalidIdentifier { .. })
374 ));
375 }
376 }
377
378 #[test]
379 fn system_table_qualifiers_validate_each_identifier() {
380 use sqlparser::ast::{Ident, ObjectName, ObjectNamePart};
381
382 let system = ObjectName(vec![
383 ObjectNamePart::Identifier(Ident::new("_system")),
384 ObjectNamePart::Identifier(Ident::new("audit_log")),
385 ]);
386 assert_eq!(
387 normalize_table_name(&system).expect("system table"),
388 "_system.audit_log"
389 );
390
391 let invalid = ObjectName(vec![
392 ObjectNamePart::Identifier(Ident::new("_system")),
393 ObjectNamePart::Identifier(Ident::new("audit\nlog")),
394 ]);
395 assert!(matches!(
396 normalize_table_name(&invalid),
397 Err(SqlError::InvalidIdentifier { .. })
398 ));
399 }
400}