1use super::{Result, SQLError};
10
11fn malformed(text: &str, detail: &str) -> SQLError {
12 SQLError::Diagnostic {
13 sqlstate: "22P02".into(),
14 message: format!("malformed record literal: \"{text}\""),
15 detail: Some(detail.into()),
16 hint: None,
17 }
18}
19
20pub fn parse_record_fields(
22 text: &str,
23 columns: usize,
24 mut field: impl FnMut(usize, Option<String>) -> Result<()>,
25) -> Result<()> {
26 let bytes = text.as_bytes();
27 let mut position = 0;
28 while position < bytes.len() && bytes[position].is_ascii_whitespace() {
29 position += 1;
30 }
31 if bytes.get(position) != Some(&b'(') {
32 return Err(malformed(text, "Missing left parenthesis."));
33 }
34 position += 1;
35 for column in 0..columns {
36 if column > 0 {
37 if bytes.get(position) == Some(&b',') {
38 position += 1;
39 } else {
40 return Err(malformed(text, "Too few columns."));
41 }
42 }
43 if matches!(bytes.get(position), Some(b',' | b')')) {
44 field(column, None)?;
45 continue;
46 }
47 let mut data = Vec::new();
48 let mut quoted = false;
49 loop {
50 let Some(&byte) = bytes.get(position) else {
51 return Err(malformed(text, "Unexpected end of input."));
52 };
53 if !quoted && matches!(byte, b',' | b')') {
54 break;
55 }
56 position += 1;
57 match byte {
58 b'\\' => {
59 let Some(&escaped) = bytes.get(position) else {
60 return Err(malformed(text, "Unexpected end of input."));
61 };
62 data.push(escaped);
63 position += 1;
64 }
65 b'"' if !quoted => quoted = true,
66 b'"' if bytes.get(position) == Some(&b'"') => {
67 data.push(b'"');
68 position += 1;
69 }
70 b'"' => quoted = false,
71 other => data.push(other),
72 }
73 }
74 let data = String::from_utf8(data)
75 .map_err(|_| SQLError::Internal("record field split a UTF-8 character".into()))?;
76 field(column, Some(data))?;
77 }
78 if bytes.get(position) != Some(&b')') {
79 return Err(malformed(text, "Too many columns."));
80 }
81 position += 1;
82 while position < bytes.len() && bytes[position].is_ascii_whitespace() {
83 position += 1;
84 }
85 if position < bytes.len() {
86 return Err(malformed(text, "Junk after right parenthesis."));
87 }
88 Ok(())
89}
90
91#[cfg(test)]
92mod tests {
93 use super::parse_record_fields;
94
95 fn fields(text: &str, columns: usize) -> Result<Vec<Option<String>>, String> {
96 let mut fields = Vec::new();
97 parse_record_fields(text, columns, |_, field| {
98 fields.push(field);
99 Ok(())
100 })
101 .map_err(|error| match error {
102 crate::SQLError::Diagnostic { detail, .. } => detail.unwrap_or_default(),
103 other => other.to_string(),
104 })?;
105 Ok(fields)
106 }
107
108 #[test]
109 fn fields_follow_record_in_quoting() {
110 assert_eq!(
111 fields(r#" (1,"a ""quoted"", name","{x,""y z""}") "#, 3).unwrap(),
112 vec![
113 Some("1".into()),
114 Some(r#"a "quoted", name"#.into()),
115 Some(r#"{x,"y z"}"#.into())
116 ]
117 );
118 assert_eq!(
119 fields("(,\"\",)", 3).unwrap(),
120 vec![None, Some(String::new()), None]
121 );
122 assert_eq!(
123 fields(r"(b\\c, x )", 2).unwrap(),
124 vec![Some(r"b\c".into()), Some(" x ".into())]
125 );
126 assert_eq!(fields("()", 0).unwrap(), Vec::<Option<String>>::new());
127 assert_eq!(fields("()", 1).unwrap(), vec![None]);
128 }
129
130 #[test]
131 fn malformed_literals_report_record_in_details() {
132 for (text, columns, detail) in [
133 ("1,2)", 2, "Missing left parenthesis."),
134 ("(1)", 2, "Too few columns."),
135 ("(1,2,3)", 2, "Too many columns."),
136 ("(1,2) x", 2, "Junk after right parenthesis."),
137 ("(1,\"2)", 2, "Unexpected end of input."),
138 ("(1,2\\", 2, "Unexpected end of input."),
139 ("(1", 1, "Unexpected end of input."),
140 ] {
141 assert_eq!(fields(text, columns).unwrap_err(), detail, "{text}");
142 }
143 }
144}