Skip to main content

uqa_sql/expr/composites/
input.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! The field syntax of `record_in`: parentheses around comma-separated fields, where an empty field is NULL, double quotes protect separators and whitespace, a doubled quote inside quotes is one quote, and a backslash takes the next character literally.
8
9use 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
20/// Split `text` into `columns` fields as `record_in` does, handing each field to `field` as soon as it is read: `None` is a NULL field. Leading and trailing whitespace around the parentheses is allowed; whitespace inside an unquoted field belongs to the field.
21pub 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}