Generic binary-operator rewriter: replaces all occurrences of `op` (outside literals/comments/quoted idents) with `func_name(left, right)`.
(sql: &str, op: &str, func_name: &str)
| 104 | /// Generic binary-operator rewriter: replaces all occurrences of `op` |
| 105 | /// (outside literals/comments/quoted idents) with `func_name(left, right)`. |
| 106 | fn rewrite_binary_op(sql: &str, op: &str, func_name: &str) -> Option<String> { |
| 107 | if !has_operator_outside_literals(sql, op) { |
| 108 | return None; |
| 109 | } |
| 110 | |
| 111 | let positions = find_operator_positions(sql, op); |
| 112 | if positions.is_empty() { |
| 113 | return None; |
| 114 | } |
| 115 | |
| 116 | let mut result = String::with_capacity(sql.len()); |
| 117 | let mut consumed = 0usize; |
| 118 | let mut found = false; |
| 119 | |
| 120 | for op_pos in positions { |
| 121 | if op_pos < consumed { |
| 122 | continue; |
| 123 | } |
| 124 | |
| 125 | let before = &sql[consumed..op_pos]; |
| 126 | let left = extract_left_operand(before)?; |
| 127 | // The left operand is the trailing identifier of `before`. Subtract |
| 128 | // its length from the *trimmed* end of `before` — using `before.len()` |
| 129 | // directly is off-by-one whenever there is whitespace between the |
| 130 | // column and the operator (e.g. `embedding <-> ARRAY[...]`), which |
| 131 | // would leave the column's first character behind in the result and |
| 132 | // corrupt the rewritten function name (`evector_distance(...)`). |
| 133 | let trimmed_end_len = before.trim_end().len(); |
| 134 | let left_start = consumed + (trimmed_end_len - left.len()); |
| 135 | |
| 136 | let after_op = &sql[op_pos + op.len()..]; |
| 137 | let (right, right_len) = extract_right_operand(after_op.trim_start())?; |
| 138 | let ws_skip = after_op.len() - after_op.trim_start().len(); |
| 139 | |
| 140 | result.push_str(&sql[consumed..left_start]); |
| 141 | result.push_str(&format!("{func_name}({left}, {right})")); |
| 142 | consumed = op_pos + op.len() + ws_skip + right_len; |
| 143 | found = true; |
| 144 | } |
| 145 | |
| 146 | if !found { |
| 147 | return None; |
| 148 | } |
| 149 | |
| 150 | result.push_str(&sql[consumed..]); |
| 151 | Some(result) |
| 152 | } |
| 153 | |
| 154 | #[cfg(test)] |
| 155 | mod tests { |
no test coverage detected