nodedb_sql/planner/select/
helpers.rs1use sqlparser::ast;
7
8use crate::error::{Result, SqlError};
9use crate::parser::normalize::{SCHEMA_QUALIFIED_MSG, normalize_ident};
10use crate::planner::cp_projection::{convert_cp_item, cp_projection_scope};
11use crate::planner::predicate_coerce::coerce_predicate_literals;
12use crate::resolver::ColumnScope;
13use crate::resolver::columns::TableScope;
14use crate::resolver::expr::convert_expr;
15use crate::types::*;
16
17pub(super) fn source_projection(plan: &SqlPlan) -> Vec<Projection> {
22 match plan {
23 SqlPlan::Scan { projection, .. }
24 | SqlPlan::Join { projection, .. }
25 | SqlPlan::TextSearch(TextSearchPlan { projection, .. }) => projection.clone(),
26 _ => Vec::new(),
27 }
28}
29
30pub fn convert_projection(
38 items: &[ast::SelectItem],
39 scope: &TableScope,
40) -> Result<Vec<Projection>> {
41 let cp_scope = cp_projection_scope(scope);
42 let scope = ColumnScope::Relations(scope);
43 let mut result = Vec::new();
44 for item in items {
45 match item {
46 ast::SelectItem::UnnamedExpr(expr) => {
47 let alias = format!("{expr}").to_lowercase();
48 if let Some(cp) = convert_cp_item(expr, alias.clone(), cp_scope.as_ref())? {
49 result.push(cp);
50 continue;
51 }
52 let sql_expr = convert_expr(expr, &scope)?;
53 match &sql_expr {
54 SqlExpr::Column { table, name } => {
55 result.push(Projection::Column(qualified_name(table.as_deref(), name)));
56 }
57 SqlExpr::Wildcard => {
58 result.push(Projection::Star);
59 }
60 _ => {
61 result.push(Projection::Computed {
62 expr: sql_expr,
63 alias,
64 });
65 }
66 }
67 }
68 ast::SelectItem::ExprWithAlias { expr, alias } => {
69 let alias = normalize_ident(alias);
70 if let Some(cp) = convert_cp_item(expr, alias.clone(), cp_scope.as_ref())? {
71 result.push(cp);
72 continue;
73 }
74 let sql_expr = convert_expr(expr, &scope)?;
75 result.push(Projection::Computed {
76 expr: sql_expr,
77 alias,
78 });
79 }
80 ast::SelectItem::ExprWithAliases { aliases, .. } => {
86 return Err(SqlError::Unsupported {
87 detail: format!(
88 "multi-alias projection ('AS' with {} names) is not supported; \
89 give the expression a single alias",
90 aliases.len()
91 ),
92 });
93 }
94 ast::SelectItem::Wildcard(_) => {
95 result.push(Projection::Star);
96 }
97 ast::SelectItem::QualifiedWildcard(kind, _) => {
98 let table_name = match kind {
99 ast::SelectItemQualifiedWildcardKind::ObjectName(name) => {
100 crate::parser::normalize::normalize_object_name_checked(name)?
101 }
102 _ => String::new(),
103 };
104 result.push(Projection::QualifiedStar(table_name));
105 }
106 }
107 }
108 Ok(result)
109}
110
111pub fn qualified_name(table: Option<&str>, name: &str) -> String {
113 table.map_or_else(|| name.to_string(), |table| format!("{table}.{name}"))
114}
115
116pub fn convert_where_to_filters(expr: &ast::Expr, scope: &TableScope) -> Result<Vec<Filter>> {
124 let mut sql_expr = canonicalize_predicate(convert_expr(expr, &ColumnScope::Relations(scope))?);
125 coerce_predicate_literals(&mut sql_expr, scope)?;
126 Ok(vec![Filter {
127 expr: FilterExpr::Expr(sql_expr),
128 }])
129}
130
131fn canonicalize_predicate(expr: SqlExpr) -> SqlExpr {
145 match expr {
146 SqlExpr::BinaryOp { left, op, right } => {
147 let left = canonicalize_predicate(*left);
148 let right = canonicalize_predicate(*right);
149 if let Some(flipped) = flip_comparison(op)
153 && !matches!(left, SqlExpr::Column { .. })
154 && matches!(right, SqlExpr::Column { .. })
155 {
156 return SqlExpr::BinaryOp {
157 left: Box::new(right),
158 op: flipped,
159 right: Box::new(left),
160 };
161 }
162 SqlExpr::BinaryOp {
163 left: Box::new(left),
164 op,
165 right: Box::new(right),
166 }
167 }
168 SqlExpr::UnaryOp { op, expr } => SqlExpr::UnaryOp {
169 op,
170 expr: Box::new(canonicalize_predicate(*expr)),
171 },
172 other => other,
173 }
174}
175
176fn flip_comparison(op: BinaryOp) -> Option<BinaryOp> {
180 match op {
181 BinaryOp::Eq => Some(BinaryOp::Eq),
182 BinaryOp::Ne => Some(BinaryOp::Ne),
183 BinaryOp::Gt => Some(BinaryOp::Lt),
184 BinaryOp::Ge => Some(BinaryOp::Le),
185 BinaryOp::Lt => Some(BinaryOp::Gt),
186 BinaryOp::Le => Some(BinaryOp::Ge),
187 BinaryOp::Add
188 | BinaryOp::Sub
189 | BinaryOp::Mul
190 | BinaryOp::Div
191 | BinaryOp::Mod
192 | BinaryOp::And
193 | BinaryOp::Or
194 | BinaryOp::Concat => None,
195 }
196}
197
198pub fn extract_func_args(func: &ast::Function) -> Result<Vec<ast::Expr>> {
199 match &func.args {
200 ast::FunctionArguments::List(args) => Ok(args
201 .args
202 .iter()
203 .filter_map(|a| match a {
204 ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Expr(e)) => Some(e.clone()),
205 _ => None,
206 })
207 .collect()),
208 _ => Ok(Vec::new()),
209 }
210}
211
212pub(super) fn extract_column_name(expr: &ast::Expr) -> Result<String> {
213 match expr {
214 ast::Expr::Identifier(ident) => Ok(normalize_ident(ident)),
215 ast::Expr::CompoundIdentifier(parts) if parts.len() >= 3 => {
216 let qualified: String = parts
217 .iter()
218 .map(normalize_ident)
219 .collect::<Vec<_>>()
220 .join(".");
221 Err(SqlError::Unsupported {
222 detail: format!(
223 "schema-qualified column reference '{qualified}': {SCHEMA_QUALIFIED_MSG}"
224 ),
225 })
226 }
227 ast::Expr::CompoundIdentifier(parts) => Ok(parts
228 .iter()
229 .map(normalize_ident)
230 .collect::<Vec<_>>()
231 .join(".")),
232 _ => Err(SqlError::Unsupported {
233 detail: format!("expected column name, got: {expr}"),
234 }),
235 }
236}
237
238pub fn extract_string_literal(expr: &ast::Expr) -> Result<String> {
239 match expr {
240 ast::Expr::Value(v) => match &v.value {
241 ast::Value::SingleQuotedString(s) => Ok(s.clone()),
242 _ => Err(SqlError::Unsupported {
243 detail: format!("expected string literal, got: {expr}"),
244 }),
245 },
246 _ => Err(SqlError::Unsupported {
247 detail: format!("expected string literal, got: {expr}"),
248 }),
249 }
250}
251
252pub fn extract_float(expr: &ast::Expr) -> Result<f64> {
253 match expr {
254 ast::Expr::Value(v) => match &v.value {
255 ast::Value::Number(n, _) => n.parse::<f64>().map_err(|_| SqlError::TypeMismatch {
256 detail: format!("expected number: {n}"),
257 }),
258 _ => Err(SqlError::TypeMismatch {
259 detail: format!("expected number, got: {expr}"),
260 }),
261 },
262 ast::Expr::UnaryOp {
264 op: ast::UnaryOperator::Minus,
265 expr: inner,
266 } => extract_float(inner).map(|f| -f),
267 _ => Err(SqlError::TypeMismatch {
268 detail: format!("expected number, got: {expr}"),
269 }),
270 }
271}
272
273pub(super) fn metric_from_func_name(name: &str) -> DistanceMetric {
281 if name.eq_ignore_ascii_case("vector_cosine_distance") {
282 DistanceMetric::Cosine
283 } else if name.eq_ignore_ascii_case("vector_neg_inner_product") {
284 DistanceMetric::InnerProduct
285 } else {
286 DistanceMetric::L2
287 }
288}
289
290pub(super) fn extract_float_array(expr: &ast::Expr) -> Result<Vec<f32>> {
293 match expr {
294 ast::Expr::Array(ast::Array { elem, .. }) => elem
295 .iter()
296 .map(|e| extract_float(e).map(|f| f as f32))
297 .collect(),
298 ast::Expr::Function(func) => {
299 let name = func
300 .name
301 .0
302 .iter()
303 .map(|p| match p {
304 ast::ObjectNamePart::Identifier(ident) => normalize_ident(ident),
305 _ => String::new(),
306 })
307 .collect::<Vec<_>>()
308 .join(".");
309 if name == "make_array" || name == "array" {
310 let args = extract_func_args(func)?;
311 args.iter()
312 .map(|e| extract_float(e).map(|f| f as f32))
313 .collect()
314 } else {
315 Err(SqlError::Unsupported {
316 detail: format!("expected array, got function: {name}"),
317 })
318 }
319 }
320 ast::Expr::Value(v) => {
324 let s = match &v.value {
325 sqlparser::ast::Value::SingleQuotedString(s) => s.as_str(),
326 sqlparser::ast::Value::DoubleQuotedString(s) => s.as_str(),
327 _ => {
328 return Err(SqlError::Unsupported {
329 detail: format!("expected array literal, got: {expr}"),
330 });
331 }
332 };
333 let trimmed = s.trim();
334 if !trimmed.starts_with('[') || !trimmed.ends_with(']') {
335 return Err(SqlError::Unsupported {
336 detail: format!("expected JSON array string, got: {s:?}"),
337 });
338 }
339 let inner = &trimmed[1..trimmed.len() - 1];
340 if inner.trim().is_empty() {
341 return Ok(Vec::new());
342 }
343 inner
344 .split(',')
345 .map(|part| {
346 part.trim()
347 .parse::<f32>()
348 .map_err(|_| SqlError::Unsupported {
349 detail: format!("cannot parse float from array element: {part:?}"),
350 })
351 })
352 .collect()
353 }
354 _ => Err(SqlError::Unsupported {
355 detail: format!("expected array literal, got: {expr}"),
356 }),
357 }
358}