Skip to main content

linkrs_query/cache/plan_cache/
params.rs

1use super::key::ParamPosition;
2
3/// Parameterized query processor
4///
5/// Handling the parsing and binding of parameterized queries
6pub struct ParameterizedQueryHandler;
7
8impl ParameterizedQueryHandler {
9    /// Create a new parametric query processor.
10    pub fn new() -> Self {
11        Self
12    }
13
14    /// Extract the parameter positions from the query.
15    ///
16    /// # Parameters
17    /// - `query`: query text
18    ///
19    /// # Returns
20    /// Parameter Location List
21    pub fn extract_params(&self, query: &str) -> Vec<ParamPosition> {
22        self.extract_param_matches(query)
23            .into_iter()
24            .map(|(position, _)| position)
25            .collect()
26    }
27
28    /// Extract the parameter matches from the query together with their end
29    /// offsets. Assignment left-hand sides (`$var = ...`) are excluded, and
30    /// `@name` / `$N` occurrences inside string literals (`'...'` / `"..."`,
31    /// backslash escapes respected) are ignored so that literal content such
32    /// as an e-mail address does not look like a parameter.
33    fn extract_param_matches(&self, query: &str) -> Vec<(ParamPosition, usize)> {
34        let mut positions = Vec::new();
35        let chars: Vec<char> = query.chars().collect();
36        let mut i = 0usize;
37        let mut idx = 0usize;
38        let mut in_string: Option<char> = None;
39
40        while i < chars.len() {
41            let c = chars[i];
42
43            if let Some(quote) = in_string {
44                if c == '\\' {
45                    i = (i + 2).min(chars.len());
46                    continue;
47                }
48                if c == quote {
49                    in_string = None;
50                }
51                i += 1;
52                continue;
53            }
54
55            match c {
56                '\'' | '"' => {
57                    in_string = Some(c);
58                    i += 1;
59                }
60                '@' => {
61                    let mut j = i + 1;
62                    if j < chars.len() && (chars[j].is_ascii_alphabetic() || chars[j] == '_') {
63                        j += 1;
64                        while j < chars.len()
65                            && (chars[j].is_ascii_alphanumeric() || chars[j] == '_')
66                        {
67                            j += 1;
68                        }
69                        let name: String = chars[i + 1..j].iter().collect();
70                        // Skip the left-hand side of a variable assignment,
71                        // e.g. `$result = GO ...` defines a session variable
72                        // instead of declaring a named query parameter.
73                        let after_match = &query[j..];
74                        if !after_match.trim_start().starts_with('=') {
75                            positions.push((
76                                ParamPosition {
77                                    index: idx,
78                                    name: Some(name),
79                                    position: i,
80                                    expected_type: None,
81                                },
82                                j,
83                            ));
84                            idx += 1;
85                        }
86                        i = j;
87                    } else {
88                        i += 1;
89                    }
90                }
91                '$' => {
92                    let mut j = i + 1;
93                    let start = j;
94                    while j < chars.len() && chars[j].is_ascii_digit() {
95                        j += 1;
96                    }
97                    if j > start {
98                        let digits: String = chars[start..j].iter().collect();
99                        let after_match = &query[j..];
100                        if !after_match.trim_start().starts_with('=') {
101                            let parsed = digits.parse::<usize>().unwrap_or(idx);
102                            positions.push((
103                                ParamPosition {
104                                    index: parsed,
105                                    name: None,
106                                    position: i,
107                                    expected_type: None,
108                                },
109                                j,
110                            ));
111                            idx += 1;
112                        }
113                        i = j;
114                    } else {
115                        i += 1;
116                    }
117                }
118                _ => i += 1,
119            }
120        }
121
122        positions
123    }
124
125    /// Parameterize the query (replace parameters with placeholders)
126    ///
127    /// # Parameters
128    /// - `query`: query text
129    ///
130    /// # Returns
131    /// (parameterized query, parameter list)
132    pub fn parameterize(&self, query: &str) -> (String, Vec<ParamPosition>) {
133        let matches = self.extract_param_matches(query);
134        let positions = matches
135            .iter()
136            .map(|(position, _)| position.clone())
137            .collect::<Vec<_>>();
138
139        // Replace only the matches that were accepted as parameters so that
140        // assignment left-hand sides ($var = ...) stay intact in the template.
141        let mut parameterized = String::with_capacity(query.len());
142        let mut last_end = 0;
143        for (position, end) in matches {
144            parameterized.push_str(&query[last_end..position.position]);
145            parameterized.push('?');
146            last_end = end;
147        }
148        parameterized.push_str(&query[last_end..]);
149        (parameterized, positions)
150    }
151}
152
153impl Default for ParameterizedQueryHandler {
154    fn default() -> Self {
155        Self::new()
156    }
157}