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 1044228 : pub fn tolower_fuzzy(c: u8) -> u8 {
9 1044228 : c.to_ascii_lowercase()
10 1044228 : }
11 :
12 : /// Compare two byte strings case-insensitively
13 133667 : pub fn ci_cmp(a: &[u8], b: &[u8]) -> std::cmp::Ordering {
14 203899 : for (x, y) in a.iter().zip(b) {
15 203899 : match tolower_fuzzy(*x).cmp(&tolower_fuzzy(*y)) {
16 77198 : std::cmp::Ordering::Equal => continue,
17 126701 : other => return other,
18 : }
19 : }
20 6966 : a.len().cmp(&b.len())
21 133667 : }
22 :
23 : /// Does `text` start with `prefix`, ignoring ASCII case?
24 302672 : pub fn ci_starts_with(text: &[u8], prefix: &[u8]) -> bool {
25 318215 : text.len() >= prefix.len() && text.iter().zip(prefix).all(|(a, b)| tolower_fuzzy(*a) == tolower_fuzzy(*b))
26 302672 : }
27 :
28 : /// Find `needle` in `haystack`, ignoring ASCII case
29 69234 : pub fn ci_find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
30 69234 : if needle.is_empty() {
31 0 : return Some(0);
32 69234 : }
33 69234 : if needle.len() > haystack.len() {
34 8303 : return None;
35 60931 : }
36 301634 : (0..=haystack.len() - needle.len()).find(|&i| ci_starts_with(&haystack[i..], needle))
37 69234 : }
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 106 : pub fn unquote_string_literal(literal: &str) -> Option<String> {
47 106 : let inner = literal.strip_prefix('\'')?.strip_suffix('\'')?;
48 106 : Some(inner.replace("''", "'"))
49 106 : }
50 :
51 : /// Quote an identifier if it would not round-trip unquoted
52 5893 : pub fn quote_identifier(text: &str) -> String {
53 38810 : let is_start = |c: u8| c.is_ascii_alphabetic() || c == b'_' || c >= 0x80;
54 32968 : let is_cont = |c: u8| is_start(c) || c.is_ascii_digit() || c == b'$';
55 5893 : let bytes = text.as_bytes();
56 5893 : let requires_quotes = bytes.is_empty()
57 33074 : || bytes.iter().any(|c| c.is_ascii_uppercase())
58 5843 : || !is_start(bytes[0])
59 32967 : || !bytes.iter().all(|&c| is_cont(c));
60 5893 : if !requires_quotes {
61 5823 : return text.to_string();
62 70 : }
63 70 : let mut out = String::with_capacity(text.len() + 2);
64 70 : out.push('"');
65 615 : for c in text.chars() {
66 615 : if c == '"' {
67 3 : out.push('"');
68 612 : }
69 615 : out.push(c);
70 : }
71 70 : out.push('"');
72 70 : out
73 5893 : }
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 : }
|