Skip to main content

uqa_sql/semantics/parameters/
identifier_list.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Identifier lists in parameter values, such as `search_path`, split as `PostgreSQL`'s `SplitIdentifierString` does.
8
9use super::units::is_c_space;
10
11/// The longest identifier, `NAMEDATALEN - 1` bytes.
12const MAX_IDENTIFIER_BYTES: usize = 63;
13
14/// Split `text` into identifiers separated by `separator`: a double-quoted name keeps its case and collapses doubled quotes, an unquoted name runs to the separator or white space and is downcased, and every name is truncated to the length of an identifier. An empty or blank text is an empty list; `None` for invalid list syntax.
15pub fn split_identifier_list(text: &str, separator: u8) -> Option<Vec<String>> {
16    let bytes = text.as_bytes();
17    let mut index = skip_space(bytes, 0);
18    let mut names = Vec::new();
19    if index == bytes.len() {
20        return Some(names);
21    }
22    loop {
23        let name = if bytes.get(index) == Some(&b'"') {
24            let mut name = Vec::new();
25            index += 1;
26            loop {
27                let quote = index + bytes[index..].iter().position(|byte| *byte == b'"')?;
28                name.extend_from_slice(&bytes[index..quote]);
29                if bytes.get(quote + 1) == Some(&b'"') {
30                    name.push(b'"');
31                    index = quote + 2;
32                } else {
33                    index = quote + 1;
34                    break;
35                }
36            }
37            String::from_utf8(name).expect("split at ASCII quotes")
38        } else {
39            let start = index;
40            while index < bytes.len() && bytes[index] != separator && !is_c_space(bytes[index]) {
41                index += 1;
42            }
43            if index == start {
44                return None;
45            }
46            text[start..index].to_ascii_lowercase()
47        };
48        index = skip_space(bytes, index);
49        let done = match bytes.get(index) {
50            Some(byte) if *byte == separator => {
51                index = skip_space(bytes, index + 1);
52                false
53            }
54            None => true,
55            Some(_) => return None,
56        };
57        names.push(truncate(name));
58        if done {
59            return Some(names);
60        }
61    }
62}
63
64fn skip_space(bytes: &[u8], mut index: usize) -> usize {
65    while bytes.get(index).copied().is_some_and(is_c_space) {
66        index += 1;
67    }
68    index
69}
70
71fn truncate(mut name: String) -> String {
72    if name.len() > MAX_IDENTIFIER_BYTES {
73        let mut end = MAX_IDENTIFIER_BYTES;
74        while !name.is_char_boundary(end) {
75            end -= 1;
76        }
77        name.truncate(end);
78    }
79    name
80}
81
82#[cfg(test)]
83mod tests;