Line data Source code
1 : //! Utilities shared by the modules of the core.
2 :
3 : pub mod rope;
4 : pub mod topk;
5 :
6 : /// ASCII lower-casing
7 : #[inline]
8 1109690 : pub fn tolower_fuzzy(c: u8) -> u8 {
9 1109690 : c.to_ascii_lowercase()
10 1109690 : }
11 :
12 : /// Compare two byte strings case-insensitively
13 143521 : pub fn ci_cmp(a: &[u8], b: &[u8]) -> std::cmp::Ordering {
14 218762 : for (x, y) in a.iter().zip(b) {
15 218762 : match tolower_fuzzy(*x).cmp(&tolower_fuzzy(*y)) {
16 84152 : std::cmp::Ordering::Equal => continue,
17 134610 : other => return other,
18 : }
19 : }
20 8911 : a.len().cmp(&b.len())
21 143521 : }
22 :
23 : /// Does `text` start with `prefix`, ignoring ASCII case?
24 319525 : pub fn ci_starts_with(text: &[u8], prefix: &[u8]) -> bool {
25 336083 : text.len() >= prefix.len() && text.iter().zip(prefix).all(|(a, b)| tolower_fuzzy(*a) == tolower_fuzzy(*b))
26 319525 : }
27 :
28 : /// Find `needle` in `haystack`, ignoring ASCII case
29 72420 : pub fn ci_find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
30 72420 : if needle.is_empty() {
31 0 : return Some(0);
32 72420 : }
33 72420 : if needle.len() > haystack.len() {
34 8405 : return None;
35 64015 : }
36 318411 : (0..=haystack.len() - needle.len()).find(|&i| ci_starts_with(&haystack[i..], needle))
37 72420 : }
38 :
39 : /// A string literal of a text, quotes are doubled: `it's` is `'it''s'`
40 15 : pub fn quote_string_literal(text: &str) -> String {
41 15 : format!("'{}'", text.replace('\'', "''"))
42 15 : }
43 :
44 : /// The text of a string literal as the scanner reads it, `'it''s'` is `it's`. None for other literals, e.g. the
45 : /// escape strings `E'...'`.
46 256 : pub fn unquote_string_literal(literal: &str) -> Option<String> {
47 256 : let inner = literal.strip_prefix('\'')?.strip_suffix('\'')?;
48 256 : Some(inner.replace("''", "'"))
49 256 : }
50 :
51 : /// Quote an identifier if it would not round-trip unquoted
52 10623 : pub fn quote_identifier(text: &str) -> String {
53 72975 : let is_start = |c: u8| c.is_ascii_alphabetic() || c == b'_' || c >= 0x80;
54 62407 : let is_cont = |c: u8| is_start(c) || c.is_ascii_digit() || c == b'$';
55 10623 : let bytes = text.as_bytes();
56 10623 : let requires_quotes = bytes.is_empty()
57 62531 : || bytes.iter().any(|c| c.is_ascii_uppercase())
58 10568 : || !is_start(bytes[0])
59 62407 : || !bytes.iter().all(|&c| is_cont(c));
60 10623 : if !requires_quotes {
61 10544 : return text.to_string();
62 79 : }
63 79 : let mut out = String::with_capacity(text.len() + 2);
64 79 : out.push('"');
65 689 : for c in text.chars() {
66 689 : if c == '"' {
67 3 : out.push('"');
68 686 : }
69 689 : out.push(c);
70 : }
71 79 : out.push('"');
72 79 : out
73 10623 : }
74 :
75 : /// Quote a column name for a column reference in SQL text, e.g. the projection of a query result. Quotes like
76 : /// `quote_identifier` and also quotes the keywords that are not unreserved: `select user` reads the current user and
77 : /// `select order` does not parse, the columns are `"user"` and `"order"`.
78 10 : pub fn quote_column_name(text: &str) -> String {
79 : use dashql_parser::keywords::{Keyword, KeywordCategory};
80 10 : let quoted = quote_identifier(text);
81 10 : if quoted != text {
82 2 : return quoted;
83 8 : }
84 8 : match Keyword::find(text.as_bytes()) {
85 7 : Some(keyword) if keyword.category != KeywordCategory::SqlUnreserved => format!("\"{text}\""),
86 3 : _ => quoted,
87 : }
88 10 : }
|