real_time_sqlx/
utils.rs

1use std::{fmt, iter::repeat};
2
3/// Utility function to format a list of displayable items with a specific
4/// separator
5///
6/// Example:
7/// 1, 2, 3, condition1 OR condition2 OR condition3
8#[inline]
9pub(crate) fn format_list<T: fmt::Display>(items: &[T], separator: &str) -> String {
10    items
11        .iter()
12        .map(|item| format!("{}", item).to_string())
13        .collect::<Vec<String>>()
14        .join(separator)
15}
16
17/// Utility function to format an iterator of displayable items with a
18/// specific separator
19///
20/// Example:
21/// 1, 2, 3, condition1 OR condition2 OR condition3
22#[inline]
23pub(crate) fn format_iter<T: fmt::Display, I: IntoIterator<Item = T>>(
24    items: I,
25    separator: &str,
26) -> String {
27    items
28        .into_iter()
29        .map(|item| format!("{}", item).to_string())
30        .collect::<Vec<String>>()
31        .join(separator)
32}
33
34/// Create an owned vector of keys from a JSON object.
35/// The vector is not actually "ordered", rather it enables reading the values
36/// of multiple similar objects always in the same order for SQL insertion.
37#[inline]
38pub(crate) fn ordered_keys(object: &serde_json::Map<String, serde_json::Value>) -> Vec<String> {
39    object.keys().map(|key| (*key).clone()).collect()
40}
41
42/// Convert a string with '?' placeholders to numbered '$1' placeholderss
43#[inline]
44pub(crate) fn to_numbered_placeholders(query: &str) -> String {
45    let mut result = String::new();
46    let mut counter = 1;
47
48    for c in query.chars() {
49        if c == '?' {
50            result.push_str(&format!("${counter}"));
51            counter += 1;
52        } else {
53            result.push(c);
54        }
55    }
56
57    result
58}
59
60/// Create a placeholder string (?, ?, ?) for a given count of placeholders,
61/// for one value
62#[inline]
63pub(crate) fn placeholders(count: usize) -> String {
64    let str_placeholders = repeat("?".to_string())
65        .take(count)
66        .collect::<Vec<String>>()
67        .join(", ");
68
69    format!("({str_placeholders})")
70}
71
72/// Create a placeholder string (?, ?, ?), (?, ?, ?), (?, ?, ?) for a given
73/// count of placeholders, for n values
74#[inline]
75pub(crate) fn repeat_placeholders(count: usize, n_repeat: usize) -> String {
76    repeat(placeholders(count))
77        .take(n_repeat)
78        .collect::<Vec<String>>()
79        .join(", ")
80}
81
82/// Sanitize table and column names to avoid SQL injection
83/// Only letters, numbers and underscores are allowed. No spaces
84#[inline]
85pub(crate) fn sanitize_identifier(str: &str) -> String {
86    str.replace(|c: char| !c.is_alphanumeric() && c != '_', "")
87}
88
89/// Generate an UPDATE statement from a table name and a list of keys
90#[inline]
91pub(crate) fn update_statement(table: &str, keys: &[String]) -> String {
92    let table = sanitize_identifier(table);
93    let columns = keys
94        .iter()
95        .map(|key| format!("\"{}\" = ?", sanitize_identifier(key)))
96        .collect::<Vec<String>>()
97        .join(", ");
98
99    format!("UPDATE {table} SET {columns} WHERE id = ? RETURNING *")
100}
101
102/// Generate an INSERT statement from a table name and a list of keys
103#[inline]
104pub(crate) fn insert_statement(table: &str, keys: &[String]) -> String {
105    let table = sanitize_identifier(table);
106    let values_placeholders = placeholders(keys.len());
107    let columns = format_iter(keys.iter().map(|s| sanitize_identifier(s)), ", ");
108
109    format!("INSERT INTO {table} ({columns}) VALUES {values_placeholders} RETURNING *")
110}
111
112/// Generate an INSERT statement from a table name and a list of keys
113/// to insert multiple rows at once
114#[inline]
115pub(crate) fn insert_many_statement(table: &str, keys: &[String], n_rows: usize) -> String {
116    let table = sanitize_identifier(table);
117    let values_placeholders = repeat_placeholders(keys.len(), n_rows);
118    let columns = format_iter(keys.iter().map(|s| sanitize_identifier(s)), ", ");
119
120    format!("INSERT INTO {table} ({columns}) VALUES {values_placeholders} RETURNING *")
121}
122
123/// Generate a DELETE statement from a table name and an id
124#[inline]
125pub(crate) fn delete_statement(table: &str) -> String {
126    let table = sanitize_identifier(table);
127
128    format!("DELETE FROM {table} WHERE id = ? RETURNING *")
129}
130
131/// SQL-like implementation of the LIKE operator
132/// '_' matches any single character
133/// '%' matches zero or more characters
134pub(crate) fn sql_like(filter: &str, value: &str) -> bool {
135    // Helper function to perform recursive pattern matching
136    fn match_helper(f: &[char], v: &[char]) -> bool {
137        match (f, v) {
138            // If both filter and value are empty, it's a match
139            ([], []) => true,
140
141            // If filter has '%', it can match zero or more characters
142            ([first, rest @ ..], value) if *first == '%' => {
143                // Match zero characters or keep consuming value characters
144                match_helper(rest, value) || (!value.is_empty() && match_helper(f, &value[1..]))
145            }
146
147            // If filter has '_', it matches exactly one character if value is not empty
148            ([first, rest @ ..], [_, v_rest @ ..]) if *first == '_' => match_helper(rest, v_rest),
149
150            // If the current characters of both filter and value match, proceed
151            ([first, rest @ ..], [v_first, v_rest @ ..]) if first == v_first => {
152                match_helper(rest, v_rest)
153            }
154
155            // If nothing matches, return false
156            _ => false,
157        }
158    }
159
160    // Convert both filter and value to character slices for easier handling
161    match_helper(
162        &filter.chars().collect::<Vec<_>>(),
163        &value.chars().collect::<Vec<_>>(),
164    )
165}
166
167/// SQL-like implementation of the ILIKE operator
168pub(crate) fn sql_ilike(filter: &str, value: &str) -> bool {
169    sql_like(&filter.to_lowercase(), &value.to_lowercase())
170}
171
172#[cfg(test)]
173mod test_utils {
174    use super::sql_like;
175
176    #[test]
177    /// The sql_like function was generated with ChatGPT
178    /// This test guarantees that the function works as expected
179    fn test_sql_like() {
180        assert!(sql_like("he_lo", "hello"));
181        assert!(sql_like("h%o", "hello"));
182        assert!(!sql_like("h%o", "hi"));
183        assert!(sql_like("%", "anything"));
184        assert!(sql_like("_____", "12345"));
185        assert!(sql_like("_%_", "abc"));
186        assert!(sql_like("h_llo", "hello"));
187        assert!(!sql_like("he_lo", "heeeelo"));
188    }
189}