1use pg_query::NodeEnum;
9use pg_query::protobuf::{AConst, ColumnRef, DefElem, Node, RangeVar, TypeName, a_const};
10
11use crate::identifier::{Identifier, QualifiedName};
12use crate::ir::column_type::ColumnType;
13use crate::ir::default_expr::{DefaultExpr, LiteralValue};
14use crate::parse::error::{ParseError, SourceLocation};
15use crate::parse::normalize_expr;
16
17pub fn resolve_qname(
23 range_var: &RangeVar,
24 default_schema: Option<&Identifier>,
25 location: &SourceLocation,
26) -> Result<QualifiedName, ParseError> {
27 let name = ident(&range_var.relname, location)?;
28 if !range_var.schemaname.is_empty() {
29 let schema = ident(&range_var.schemaname, location)?;
30 return Ok(QualifiedName::new(schema, name));
31 }
32 default_schema.map_or_else(
33 || {
34 Err(ParseError::UnqualifiedName {
35 location: location.clone(),
36 })
37 },
38 |s| Ok(QualifiedName::new(s.clone(), name)),
39 )
40}
41
42pub fn qname_from_string_list(
54 nodes: &[pg_query::protobuf::Node],
55 default_schema: Option<&Identifier>,
56 location: &SourceLocation,
57) -> Result<QualifiedName, ParseError> {
58 let strings: Vec<&str> = nodes
59 .iter()
60 .map(|n| match n.node.as_ref() {
61 Some(NodeEnum::String(s)) => Ok(s.sval.as_str()),
62 other => Err(ParseError::Structural {
63 location: location.clone(),
64 message: format!(
65 "expected String node in type-name list, got {:?}",
66 other.map(std::mem::discriminant),
67 ),
68 }),
69 })
70 .collect::<Result<Vec<_>, _>>()?;
71 match strings.as_slice() {
72 [name] => {
73 let name_id = ident(name, location)?;
74 let schema = default_schema
75 .cloned()
76 .ok_or_else(|| ParseError::UnqualifiedName {
77 location: location.clone(),
78 })?;
79 Ok(QualifiedName::new(schema, name_id))
80 }
81 [schema, name] => {
82 let schema_id = ident(schema, location)?;
83 let name_id = ident(name, location)?;
84 Ok(QualifiedName::new(schema_id, name_id))
85 }
86 _ => Err(ParseError::Structural {
87 location: location.clone(),
88 message: format!(
89 "unexpected type-name list length {} (expected 1 or 2 String nodes)",
90 nodes.len()
91 ),
92 }),
93 }
94}
95
96pub fn node_string_value(node: &Node) -> Option<String> {
102 match node.node.as_ref()? {
103 NodeEnum::String(s) => Some(s.sval.clone()),
104 _ => None,
105 }
106}
107
108pub fn def_elem_string(de: &DefElem) -> Option<String> {
120 let arg = de.arg.as_ref()?;
121 match arg.node.as_ref()? {
122 NodeEnum::String(s) => Some(s.sval.clone()),
123 NodeEnum::Integer(i) => Some(i.ival.to_string()),
124 NodeEnum::Float(f) => Some(f.fval.clone()),
125 NodeEnum::List(list) => list
126 .items
127 .first()
128 .and_then(|n| n.node.as_ref())
129 .and_then(|n| {
130 if let NodeEnum::String(s) = n {
131 Some(s.sval.clone())
132 } else {
133 None
134 }
135 }),
136 _ => None,
137 }
138}
139
140pub fn ident(s: &str, location: &SourceLocation) -> Result<Identifier, ParseError> {
143 Identifier::from_unquoted(s).map_err(|e| ParseError::Ir {
144 location: location.clone(),
145 source: crate::ir::IrError::InvalidIdentifier(e.to_string()),
146 })
147}
148
149pub fn render_type_name_to_string(type_name: &TypeName) -> Option<String> {
164 let last = type_name.names.last()?.node.as_ref()?;
165 let NodeEnum::String(s) = last else {
166 return None;
167 };
168 let bare = s.sval.as_str();
169
170 let mut out = if bare == "interval" && !type_name.typmods.is_empty() {
172 render_interval_type_name(type_name)?
173 } else {
174 let mut o = bare.to_string();
175 if !type_name.typmods.is_empty() {
176 let mut args: Vec<String> = Vec::with_capacity(type_name.typmods.len());
177 for n in &type_name.typmods {
178 args.push(typmod_arg_to_string(n.node.as_ref()?)?);
179 }
180 o = format!("{o}({})", args.join(","));
181 }
182 o
183 };
184
185 for _ in 0..type_name.array_bounds.len() {
186 out.push_str("[]");
187 }
188 Some(out)
189}
190
191fn render_interval_type_name(type_name: &TypeName) -> Option<String> {
205 let mut int_args: Vec<i32> = Vec::with_capacity(2);
207 for n in &type_name.typmods {
208 match n.node.as_ref()? {
209 NodeEnum::AConst(c) => match c.val.as_ref()? {
210 a_const::Val::Ival(i) => int_args.push(i.ival),
211 _ => return None, },
213 _ => return None, }
215 }
216
217 let (fields_mask, precision): (i32, Option<u8>) = match int_args.as_slice() {
218 [mask] => (*mask, None),
219 [mask, prec] => {
220 let p = u8::try_from(*prec).ok()?;
221 (*mask, Some(p))
222 }
223 _ => return None,
224 };
225
226 let fields_str = interval_fields_from_mask(fields_mask);
227
228 Some(match (fields_str, precision) {
230 (None, None) => "interval".to_string(),
231 (None, Some(p)) => format!("interval({p})"),
232 (Some(f), None) => format!("interval {f}"),
233 (Some(f), Some(p)) => format!("interval {f}({p})"),
234 })
235}
236
237const fn interval_fields_from_mask(mask: i32) -> Option<&'static str> {
248 if mask == 0x7FFF {
250 return None;
251 }
252 match mask {
255 4 => Some("year"),
257 2 => Some("month"),
258 8 => Some("day"),
259 1024 => Some("hour"),
260 2048 => Some("minute"),
261 4096 => Some("second"),
262 6 => Some("year to month"), 1032 => Some("day to hour"), 3080 => Some("day to minute"), 7176 => Some("day to second"), 3072 => Some("hour to minute"), 7168 => Some("hour to second"), 6144 => Some("minute to second"), _ => None,
272 }
273}
274
275fn typmod_arg_to_string(node: &NodeEnum) -> Option<String> {
282 match node {
283 NodeEnum::AConst(c) => literal_arg_to_string(c),
284 NodeEnum::ColumnRef(cref) => columnref_ident(cref),
285 _ => None,
286 }
287}
288
289fn literal_arg_to_string(c: &AConst) -> Option<String> {
291 match c.val.as_ref()? {
292 a_const::Val::Ival(i) => Some(i.ival.to_string()),
293 a_const::Val::Sval(s) => Some(s.sval.clone()),
294 _ => None,
295 }
296}
297
298fn columnref_ident(cref: &ColumnRef) -> Option<String> {
303 let [field] = cref.fields.as_slice() else {
304 return None;
305 };
306 match field.node.as_ref()? {
307 NodeEnum::String(s) => Some(s.sval.clone()),
308 _ => None,
309 }
310}
311
312pub fn type_name_to_column_type(
324 type_name: &TypeName,
325 location: &SourceLocation,
326) -> Result<ColumnType, ParseError> {
327 let name_strings: Vec<&str> = type_name
329 .names
330 .iter()
331 .map(|n| match n.node.as_ref() {
332 Some(NodeEnum::String(s)) => Ok(s.sval.as_str()),
333 other => Err(ParseError::Structural {
334 location: location.clone(),
335 message: format!(
336 "expected String node in type-name list, got {:?}",
337 other.map(std::mem::discriminant),
338 ),
339 }),
340 })
341 .collect::<Result<Vec<_>, _>>()?;
342
343 if let [schema, name] = name_strings.as_slice()
351 && *schema != "pg_catalog"
352 {
353 let name_lower = name.to_ascii_lowercase();
354 if (name_lower == "geometry" || name_lower == "geography") && !type_name.typmods.is_empty()
355 {
356 let mut args: Vec<String> = Vec::with_capacity(type_name.typmods.len());
360 for n in &type_name.typmods {
361 let arg = typmod_arg_to_string(n.node.as_ref().ok_or_else(|| {
362 ParseError::Structural {
363 location: location.clone(),
364 message: "missing node in typmod list".into(),
365 }
366 })?)
367 .ok_or_else(|| ParseError::Structural {
368 location: location.clone(),
369 message: "could not stringify typmod argument".into(),
370 })?;
371 args.push(arg);
372 }
373 let s = format!("{schema}.{name_lower}({})", args.join(","));
374 return ColumnType::parse_from_pg_type_string(&s).map_err(|e| ParseError::Structural {
375 location: location.clone(),
376 message: format!("invalid column type {s:?}: {e}"),
377 });
378 }
379
380 let schema_id = ident(schema, location)?;
381 let name_id = ident(name, location)?;
382 return Ok(ColumnType::UserDefined(QualifiedName::new(
383 schema_id, name_id,
384 )));
385 }
386
387 let s = render_type_name_to_string(type_name).ok_or_else(|| ParseError::Structural {
388 location: location.clone(),
389 message: "could not stringify type name".into(),
390 })?;
391 ColumnType::parse_from_pg_type_string(&s).map_err(|e| ParseError::Structural {
392 location: location.clone(),
393 message: format!("invalid column type {s:?}: {e}"),
394 })
395}
396
397pub fn build_default_expr(
406 node: &NodeEnum,
407 target_type: Option<&ColumnType>,
408 default_schema: Option<&Identifier>,
409 location: &SourceLocation,
410) -> Result<DefaultExpr, ParseError> {
411 if let Some(lit) = literal_from_node(node) {
412 return Ok(DefaultExpr::Literal(lit));
413 }
414 if let Some(seq) = nextval_target(node, default_schema, location)? {
415 return Ok(DefaultExpr::Sequence(seq));
416 }
417 let normalized = normalize_expr::from_pg_node(node, target_type, location)?;
418 Ok(DefaultExpr::Expr(normalized))
419}
420
421fn literal_from_node(node: &NodeEnum) -> Option<LiteralValue> {
424 let inner = unwrap_typecast(node);
425 match inner {
426 NodeEnum::AConst(c) => aconst_to_literal(c),
427 _ => None,
428 }
429}
430
431fn unwrap_typecast(node: &NodeEnum) -> &NodeEnum {
433 if let NodeEnum::TypeCast(cast) = node
434 && let Some(arg) = cast.arg.as_ref()
435 && let Some(inner) = arg.node.as_ref()
436 {
437 return inner;
438 }
439 node
440}
441
442fn aconst_to_literal(c: &AConst) -> Option<LiteralValue> {
443 if c.isnull {
444 return Some(LiteralValue::Null);
445 }
446 match c.val.as_ref()? {
447 a_const::Val::Ival(i) => Some(LiteralValue::Integer(i64::from(i.ival))),
448 a_const::Val::Fval(f) => f.fval.parse::<f64>().ok().map(LiteralValue::Float),
449 a_const::Val::Boolval(b) => Some(LiteralValue::Bool(b.boolval)),
450 a_const::Val::Sval(s) => Some(LiteralValue::Text(s.sval.clone())),
451 a_const::Val::Bsval(_) => None,
452 }
453}
454
455fn nextval_target(
458 node: &NodeEnum,
459 default_schema: Option<&Identifier>,
460 location: &SourceLocation,
461) -> Result<Option<QualifiedName>, ParseError> {
462 let inner = unwrap_typecast(node);
463 let NodeEnum::FuncCall(fc) = inner else {
464 return Ok(None);
465 };
466 let func = match fc.funcname.last().and_then(|n| n.node.as_ref()) {
467 Some(NodeEnum::String(s)) => s.sval.as_str(),
468 _ => return Ok(None),
469 };
470 if !func.eq_ignore_ascii_case("nextval") {
471 return Ok(None);
472 }
473 let arg = fc.args.first().and_then(|n| n.node.as_ref());
474 let Some(arg_node) = arg else {
475 return Ok(None);
476 };
477 let inner_arg = unwrap_typecast(arg_node);
480 let NodeEnum::AConst(c) = inner_arg else {
481 return Ok(None);
482 };
483 let Some(a_const::Val::Sval(s)) = c.val.as_ref() else {
484 return Ok(None);
485 };
486 Ok(Some(parse_qualified_seq_name(
487 &s.sval,
488 default_schema,
489 location,
490 )?))
491}
492
493fn parse_qualified_seq_name(
495 s: &str,
496 default_schema: Option<&Identifier>,
497 location: &SourceLocation,
498) -> Result<QualifiedName, ParseError> {
499 let parts: Vec<&str> = s.split('.').collect();
500 match parts.len() {
501 1 => {
502 let name = ident(parts[0], location)?;
503 let schema = default_schema
504 .cloned()
505 .ok_or_else(|| ParseError::UnqualifiedName {
506 location: location.clone(),
507 })?;
508 Ok(QualifiedName::new(schema, name))
509 }
510 2 => {
511 let schema = ident(parts[0], location)?;
512 let name = ident(parts[1], location)?;
513 Ok(QualifiedName::new(schema, name))
514 }
515 _ => Err(ParseError::Structural {
516 location: location.clone(),
517 message: format!("unsupported qualified sequence reference {s:?}"),
518 }),
519 }
520}
521
522#[cfg(test)]
523mod tests {
524 use super::*;
525 use std::path::PathBuf;
526
527 fn loc() -> SourceLocation {
528 SourceLocation::new(PathBuf::from("test.sql"), 1, 1)
529 }
530
531 fn parse_first(sql: &str) -> NodeEnum {
532 let parsed = pg_query::parse(sql).expect("parses");
533 parsed
534 .protobuf
535 .stmts
536 .into_iter()
537 .next()
538 .and_then(|raw| raw.stmt)
539 .and_then(|n| n.node)
540 .expect("at least one statement")
541 }
542
543 fn parse_select_expr(expr: &str) -> NodeEnum {
544 let n = parse_first(&format!("SELECT {expr};"));
545 let NodeEnum::SelectStmt(s) = n else { panic!() };
546 let target = s.target_list.into_iter().next().unwrap().node.unwrap();
547 let NodeEnum::ResTarget(rt) = target else {
548 panic!()
549 };
550 rt.val.unwrap().node.unwrap()
551 }
552
553 #[test]
554 fn literal_integer_default() {
555 let n = parse_select_expr("42");
556 let d = build_default_expr(&n, Some(&ColumnType::Integer), None, &loc()).unwrap();
557 assert!(matches!(d, DefaultExpr::Literal(LiteralValue::Integer(42))));
558 }
559
560 #[test]
561 fn literal_text_default() {
562 let n = parse_select_expr("'hello'");
563 let d = build_default_expr(&n, Some(&ColumnType::Text), None, &loc()).unwrap();
564 assert!(matches!(d, DefaultExpr::Literal(LiteralValue::Text(s)) if s == "hello"));
565 }
566
567 #[test]
568 fn null_default() {
569 let n = parse_select_expr("NULL");
570 let d = build_default_expr(&n, None, None, &loc()).unwrap();
571 assert!(matches!(d, DefaultExpr::Literal(LiteralValue::Null)));
572 }
573
574 #[test]
575 fn bool_default() {
576 let n = parse_select_expr("true");
577 let d = build_default_expr(&n, Some(&ColumnType::Boolean), None, &loc()).unwrap();
578 match d {
581 DefaultExpr::Literal(LiteralValue::Bool(true)) => {}
582 DefaultExpr::Expr(e) => assert!(e.canonical_text.contains("true")),
583 other => panic!("unexpected default: {other:?}"),
584 }
585 }
586
587 #[test]
588 fn nextval_qualified_default() {
589 let n = parse_select_expr("nextval('app.seq1'::regclass)");
590 let d = build_default_expr(&n, None, None, &loc()).unwrap();
591 match d {
592 DefaultExpr::Sequence(q) => assert_eq!(q.to_string(), "app.seq1"),
593 other => panic!("expected Sequence, got {other:?}"),
594 }
595 }
596
597 #[test]
598 fn nextval_unqualified_uses_default_schema() {
599 let n = parse_select_expr("nextval('seq1'::regclass)");
600 let app = Identifier::from_unquoted("app").unwrap();
601 let d = build_default_expr(&n, None, Some(&app), &loc()).unwrap();
602 match d {
603 DefaultExpr::Sequence(q) => assert_eq!(q.to_string(), "app.seq1"),
604 other => panic!("expected Sequence, got {other:?}"),
605 }
606 }
607
608 #[test]
609 fn func_call_other_than_nextval_is_expr() {
610 let n = parse_select_expr("now()");
611 let d = build_default_expr(&n, None, None, &loc()).unwrap();
612 assert!(matches!(d, DefaultExpr::Expr(_)));
613 }
614
615 #[test]
616 fn cast_to_target_strips_in_expr_arm() {
617 let n = parse_select_expr("'a' || 'b'");
620 let d = build_default_expr(&n, Some(&ColumnType::Text), None, &loc()).unwrap();
621 assert!(matches!(d, DefaultExpr::Expr(_)));
622 }
623
624 #[test]
625 fn type_name_renders_with_typmod() {
626 let stmt = parse_first("CREATE TABLE t (c varchar(50));");
628 let NodeEnum::CreateStmt(create) = stmt else {
629 panic!()
630 };
631 let elt = create.table_elts.into_iter().next().unwrap();
632 let NodeEnum::ColumnDef(col) = elt.node.unwrap() else {
633 panic!()
634 };
635 let tn = col.type_name.unwrap();
636 let s = render_type_name_to_string(&tn).unwrap();
637 let ct = ColumnType::parse_from_pg_type_string(&s).unwrap();
638 assert_eq!(ct, ColumnType::Varchar { len: Some(50) });
639 }
640
641 fn first_column_type_name(sql: &str) -> TypeName {
643 let NodeEnum::CreateStmt(create) = parse_first(sql) else {
644 panic!("expected CREATE TABLE")
645 };
646 let elt = create.table_elts.into_iter().next().unwrap();
647 let NodeEnum::ColumnDef(col) = elt.node.unwrap() else {
648 panic!("expected ColumnDef")
649 };
650 col.type_name.unwrap()
651 }
652
653 #[test]
654 fn parameterized_postgis_types_parse() {
655 let cases = [
661 ("geometry(Point,4326)", "geometry(point,4326)"),
662 (
663 "geography(MultiPolygon,4326)",
664 "geography(multipolygon,4326)",
665 ),
666 ("geometry(geometry,4326)", "geometry(geometry,4326)"),
667 ];
668 for (decl, expected_raw) in cases {
669 let tn = first_column_type_name(&format!("CREATE TABLE t (c {decl});"));
670 let ct = type_name_to_column_type(&tn, &loc())
671 .unwrap_or_else(|e| panic!("{decl} should parse, got {e:?}"));
672 assert!(
673 matches!(&ct, ColumnType::Other { raw } if raw == expected_raw),
674 "{decl} -> {ct:?}"
675 );
676 }
677 }
678
679 #[test]
685 fn schema_qualified_postgis_types_converge() {
686 let cases = [
688 ("public.geometry(Point,4326)", "public.geometry(point,4326)"),
689 (
690 "public.geography(Point,4326)",
691 "public.geography(point,4326)",
692 ),
693 (
694 "myschema.geometry(MultiPolygon,4326)",
695 "myschema.geometry(multipolygon,4326)",
696 ),
697 ];
698 for (decl, expected_raw) in cases {
699 let tn = first_column_type_name(&format!("CREATE TABLE t (g {decl});"));
700 let ct = type_name_to_column_type(&tn, &loc())
701 .unwrap_or_else(|e| panic!("{decl} should parse, got {e:?}"));
702 assert!(
703 matches!(&ct, ColumnType::Other { raw } if raw == expected_raw),
704 "{decl} — expected Other{{raw:{expected_raw:?}}}, got {ct:?}"
705 );
706
707 let catalog_input = decl; let from_catalog = ColumnType::parse_from_pg_type_string(catalog_input).unwrap();
711 assert_eq!(
712 ct, from_catalog,
713 "{decl}: source path {ct:?} != catalog path {from_catalog:?}"
714 );
715 }
716 }
717
718 #[test]
720 fn schema_qualified_no_typmod_stays_user_defined() {
721 let tn = first_column_type_name("CREATE TABLE t (g public.geometry);");
723 let ct = type_name_to_column_type(&tn, &loc()).expect("should parse");
724 assert!(
725 matches!(&ct, ColumnType::UserDefined(q) if q.to_string() == "public.geometry"),
726 "expected UserDefined(public.geometry), got {ct:?}"
727 );
728
729 let tn2 = first_column_type_name("CREATE TABLE t (x public.mytype);");
731 let ct2 = type_name_to_column_type(&tn2, &loc()).expect("should parse");
732 assert!(
733 matches!(&ct2, ColumnType::UserDefined(q) if q.to_string() == "public.mytype"),
734 "expected UserDefined(public.mytype), got {ct2:?}"
735 );
736 }
737
738 #[test]
739 fn heterogeneous_typmod_args_render() {
740 let tn = first_column_type_name("CREATE TABLE t (c mytype('foo',1,bar));");
743 let ct = type_name_to_column_type(&tn, &loc()).expect("mixed typmods should parse");
744 assert!(
745 matches!(&ct, ColumnType::Other { raw } if raw == "mytype(foo,1,bar)"),
746 "got {ct:?}"
747 );
748 }
749
750 #[test]
757 fn interval_precision_source_path_convergence() {
758 let tn = first_column_type_name("CREATE TABLE t (c interval(6));");
760 let ct = type_name_to_column_type(&tn, &loc())
761 .unwrap_or_else(|e| panic!("interval(6) should parse, got {e:?}"));
762 assert_eq!(
763 ct,
764 ColumnType::Interval {
765 fields: None,
766 precision: Some(6),
767 },
768 "interval(6) source path should yield Interval{{fields:None, precision:Some(6)}}, got {ct:?}"
769 );
770 }
771
772 #[test]
773 fn interval_fields_source_path_convergence() {
774 let tn = first_column_type_name("CREATE TABLE t (c interval hour to minute);");
776 let ct = type_name_to_column_type(&tn, &loc())
777 .unwrap_or_else(|e| panic!("interval hour to minute should parse, got {e:?}"));
778 assert_eq!(
779 ct,
780 ColumnType::Interval {
781 fields: Some("hour to minute".to_string()),
782 precision: None,
783 },
784 "interval hour to minute source path should yield Interval{{fields:Some(\"hour to minute\"), precision:None}}, got {ct:?}"
785 );
786 }
787
788 #[test]
789 fn interval_fields_and_precision_converges_from_ast() {
790 let tn = first_column_type_name("CREATE TABLE t (c interval day to second(3));");
794 let ct = type_name_to_column_type(&tn, &loc())
795 .unwrap_or_else(|e| panic!("interval day to second(3) should parse, got {e:?}"));
796 assert_eq!(
797 ct,
798 ColumnType::Interval {
799 fields: Some("day to second".to_string()),
800 precision: Some(3),
801 },
802 "interval day to second(3) source path should yield typed Interval, got {ct:?}"
803 );
804 }
805
806 #[test]
807 fn interval_bare_source_path() {
808 let tn = first_column_type_name("CREATE TABLE t (c interval);");
810 let ct = type_name_to_column_type(&tn, &loc())
811 .unwrap_or_else(|e| panic!("interval should parse, got {e:?}"));
812 assert_eq!(
813 ct,
814 ColumnType::Interval {
815 fields: None,
816 precision: None,
817 },
818 );
819 }
820}