1use crate::types::{DataType, RecordField};
23use thiserror::Error;
24
25const AMBIGUOUS: &[char] = &[',', ':', '?', '(', ')', '[', ']'];
27
28#[derive(Error, Debug)]
29#[error("cannot decode type `{input}`: {reason}")]
30pub(crate) struct HumanizedTypeError {
31 input: String,
32 reason: String,
33}
34
35impl HumanizedTypeError {
36 fn new(input: &str, reason: impl Into<String>) -> Self {
37 HumanizedTypeError {
38 input: input.to_string(),
39 reason: reason.into(),
40 }
41 }
42}
43
44pub(crate) fn parse(input: &str) -> Result<DataType, HumanizedTypeError> {
46 parse_inner(input.trim(), input)
47}
48
49fn parse_inner(s: &str, input: &str) -> Result<DataType, HumanizedTypeError> {
50 if let Some(body) = enclosed(s, "record(", ')') {
51 let mut fields = Vec::new();
52 for part in split_top_level(body) {
53 fields.push(parse_field(part, input)?);
54 }
55 if fields.is_empty() {
56 return Err(HumanizedTypeError::new(input, "record has no fields"));
57 }
58 return Ok(DataType::Record(fields));
59 }
60
61 if let Some(body) = enclosed(s, "map[", ']') {
62 let Some(arrow) = find_top_level(body, "=>") else {
63 return Err(HumanizedTypeError::new(input, "map has no value type"));
64 };
65 return Ok(DataType::Map(Box::new(parse_inner(
66 body[arrow + 2..].trim(),
67 input,
68 )?)));
69 }
70
71 if let Some(element) = s.strip_suffix("[]") {
72 return Ok(DataType::Array(Box::new(parse_inner(element, input)?)));
73 }
74 if let Some(element) = s.strip_suffix(" list") {
75 return Ok(DataType::List(Box::new(parse_inner(element, input)?)));
76 }
77
78 if s.is_empty() {
79 return Err(HumanizedTypeError::new(input, "empty type"));
80 }
81 Ok(DataType::Named(s.to_string()))
82}
83
84fn parse_field(part: &str, input: &str) -> Result<RecordField, HumanizedTypeError> {
85 let Some(sep) = find_top_level(part, ": ") else {
86 return Err(HumanizedTypeError::new(
87 input,
88 format!("record field `{}` has no type", part.trim()),
89 ));
90 };
91 let name = part[..sep].trim();
92 if name.is_empty() || name.contains(AMBIGUOUS) {
93 return Err(HumanizedTypeError::new(
94 input,
95 format!("record field name `{}` is ambiguous unquoted", name),
96 ));
97 }
98 let mut rest = part[sep + 2..].trim();
99 let nullable = rest.ends_with('?') && depth_at(rest, rest.len() - 1) == 0;
100 if nullable {
101 rest = &rest[..rest.len() - 1];
102 }
103 Ok(RecordField {
104 name: name.to_string(),
105 r#type: parse_inner(rest, input)?,
106 nullable,
107 })
108}
109
110fn enclosed<'a>(s: &'a str, prefix: &str, close: char) -> Option<&'a str> {
117 let body = s.strip_prefix(prefix)?;
118 let mut depth = 1;
119 for (i, c) in body.char_indices() {
120 match c {
121 '(' | '[' => depth += 1,
122 ')' | ']' => {
123 depth -= 1;
124 if depth == 0 {
125 return (c == close && i + c.len_utf8() == body.len()).then_some(&body[..i]);
126 }
127 }
128 _ => {}
129 }
130 }
131 None
132}
133
134fn depth_at(s: &str, at: usize) -> i32 {
136 let mut depth = 0;
137 for (i, c) in s.char_indices() {
138 if i >= at {
139 break;
140 }
141 match c {
142 '(' | '[' => depth += 1,
143 ')' | ']' => depth -= 1,
144 _ => {}
145 }
146 }
147 depth
148}
149
150fn split_top_level(s: &str) -> Vec<&str> {
152 let mut parts = Vec::new();
153 let mut depth = 0;
154 let mut start = 0;
155 for (i, c) in s.char_indices() {
156 match c {
157 '(' | '[' => depth += 1,
158 ')' | ']' => depth -= 1,
159 ',' if depth == 0 => {
160 parts.push(&s[start..i]);
161 start = i + 1;
162 }
163 _ => {}
164 }
165 }
166 if !s.is_empty() {
167 parts.push(&s[start..]);
168 }
169 parts
170}
171
172fn find_top_level(s: &str, needle: &str) -> Option<usize> {
174 let mut depth = 0;
175 for (i, c) in s.char_indices() {
176 match c {
177 '(' | '[' => depth += 1,
178 ')' | ']' => depth -= 1,
179 _ => {}
180 }
181 if depth == 0 && s[i..].starts_with(needle) {
182 return Some(i);
183 }
184 }
185 None
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[mz_ore::test]
195 fn pg_typeof_output_round_trips() {
196 for input in [
197 "integer",
198 "bigint",
199 "timestamp without time zone",
200 "character varying",
201 "record(a: integer,b: text?)",
202 "record(a: integer,b: text?,c: bigint list?)",
203 "record(a: integer,b: text?,nested: record(x: uint8,y: numeric?))",
204 "bigint list",
205 "text list list",
206 "map[text=>integer]",
207 "integer[]",
208 "record(a: integer,b: text?) list",
209 "record(a: integer,b: text?)[]",
210 ] {
211 let parsed = parse(input).unwrap_or_else(|e| panic!("{input}: {e}"));
212 assert_eq!(parsed.to_string(), input, "round trip changed {input}");
213 }
214 }
215
216 #[mz_ore::test]
217 fn map_of_array_is_not_mistaken_for_an_array_of_map() {
218 assert_eq!(
219 parse("map[text=>integer[]]").unwrap().to_string(),
220 "map[text=>integer[]]"
221 );
222 assert_eq!(
223 parse("map[text=>integer][]").unwrap().to_string(),
224 "map[text=>integer][]"
225 );
226 }
227
228 #[mz_ore::test]
229 fn nullability_is_read_per_field() {
230 let parsed = parse("record(a: integer,b: record(x: text)?)").unwrap();
231 let DataType::Record(fields) = parsed else {
232 panic!("expected a record");
233 };
234 assert!(!fields[0].nullable);
235 assert!(fields[1].nullable, "the `?` applies to the nested record");
236 }
237
238 #[mz_ore::test]
241 fn ambiguous_field_names_are_rejected() {
242 for input in ["record(a,b: integer)", "record(: integer)", "record()"] {
243 assert!(parse(input).is_err(), "{input} should not parse");
244 }
245 }
246}