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}