use serde_json::{json, Value};
use crate::schema::{Registry, Schema};
use crate::dialect::filter::{
build_filter, build_filter_with_relations, RelPredResolver, Warnings, WhereClause,
};
use crate::dialect::ir::{RowCol, RowShape, SqlStmt};
use crate::dialect::{field_is_bool, field_is_json, physical_of, Backend, ColumnRef};
use crate::pipeline::REL_PRED_PREFIX;
use super::group_agg::{self, GroupSpec};
use super::lookup_join::{resolve_join, Join};
use super::relation_agg;
use super::{col_fn, limit_offset_sql, projection_fields, q, tname};
struct PredResolver<'a> {
preds: &'a [relation_agg::SqlRelPredicate],
backend: Backend,
schema: &'a Schema,
registry: &'a Registry,
}
impl RelPredResolver for PredResolver<'_> {
fn resolve(
&self,
field: &str,
cond: &Value,
param_seq: &mut usize,
warnings: Warnings<'_>,
) -> Result<Option<WhereClause>, String> {
if !field.starts_with(REL_PRED_PREFIX) {
return Ok(None);
}
let as_name = field.rsplit_once('.').map(|(a, _)| a).unwrap_or(field);
let Some(p) = self.preds.iter().find(|p| p.as_name == as_name) else {
return Err(format!(
"关系聚合谓词代理字段 \"{field}\" 没有对应的 $lookup"
));
};
let exists = cond
.as_object()
.and_then(|o| o.get("$exists"))
.and_then(|v| v.as_bool())
.ok_or_else(|| {
format!("关系聚合谓词代理字段 \"{field}\" 的条件非法(须为 {{\"$exists\": bool}})")
})?;
Ok(Some(relation_agg::exists_clause(
self.backend,
"t",
self.schema,
self.registry,
p,
!exists,
param_seq,
warnings,
)?))
}
}
pub(super) fn lookup_extra_condition(pipeline: &[Value]) -> Option<Value> {
let mut extras: Vec<Value> = Vec::new();
for stage in pipeline {
let Some(m) = stage.get("$match") else {
continue;
};
let parts: Vec<&Value> = match m.get("$and").and_then(|a| a.as_array()) {
Some(a) => a.iter().collect(),
None => vec![m],
};
for p in parts {
let is_join_key = p.get("$expr").is_some() && p.to_string().contains("$$");
if !is_join_key {
extras.push(p.clone());
}
}
}
match extras.len() {
0 => None,
1 => Some(extras.remove(0)),
_ => Some(json!({ "$and": extras })),
}
}
#[derive(Debug, Clone)]
struct ComputeAgg {
key: String,
rel_name: String,
model: String,
local_col: String,
foreign_col: String,
op: String,
field: Option<String>,
extra: Option<Value>,
}
fn resolve_compute_agg(schema: &Schema, lo: &Value) -> Option<ComputeAgg> {
let as_name = lo.get("as").and_then(|v| v.as_str())?;
let key = as_name.strip_prefix('_')?;
let agg = schema.compute(key)?.agg.clone()?;
let (op, path) = agg
.as_object()
.and_then(|o| o.iter().next())
.map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))?;
let (rel_name, field) = match path.split_once('.') {
Some((r, f)) => (r.to_string(), Some(f.to_string())),
None => (path, None),
};
let rel_def = schema.relations.get(&rel_name)?;
let pipeline: Vec<Value> = lo
.get("pipeline")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
Some(ComputeAgg {
key: key.to_string(),
rel_name,
model: rel_def.model.clone(),
local_col: rel_def.local_field.clone(),
foreign_col: rel_def.foreign_field.clone(),
op,
field,
extra: lookup_extra_condition(&pipeline),
})
}
#[allow(clippy::too_many_arguments)]
fn collect_lookup(
schema: &Schema,
registry: &Registry,
lo: &Value,
parent: Option<usize>,
parent_path: &[String],
parent_ones: &[bool],
joins: &mut Vec<Join>,
warnings: &mut Vec<String>,
) -> Result<(), String> {
let alias = lo
.get("as")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let from = lo
.get("from")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let Some(mut j) = resolve_join(schema, registry, &alias, &from) else {
warnings.push(format!("$lookup 关系 {} 无法匹配 schema,跳过 JOIN", alias));
return Ok(());
};
let child_pipeline: Vec<Value> = lo
.get("pipeline")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
j.parent = parent;
j.path = {
let mut p = parent_path.to_vec();
p.push(j.rel_name.clone());
p
};
j.ones = {
let mut o = parent_ones.to_vec();
o.push(j.one);
o
};
j.extra = lookup_extra_condition(&child_pipeline);
j.child_sort = child_pipeline.iter().find_map(|s| s.get("$sort").cloned());
j.child_skip = child_pipeline
.iter()
.find_map(|s| s.get("$skip").and_then(|v| v.as_i64()));
j.child_limit = child_pipeline
.iter()
.find_map(|s| s.get("$limit").and_then(|v| v.as_i64()));
let idx = joins.len();
let path = j.path.clone();
let ones = j.ones.clone();
let model = j.model.clone();
joins.push(j);
let rel_schema = registry.get(&model)?.clone();
for stage in &child_pipeline {
if let Some(nested) = stage.get("$lookup") {
collect_lookup(
&rel_schema,
registry,
nested,
Some(idx),
&path,
&ones,
joins,
warnings,
)?;
}
}
Ok(())
}
fn child_order_sql(backend: Backend, rel_schema: &Schema, j: &Join) -> Result<String, String> {
let mut parts: Vec<String> = Vec::new();
if let Some(s) = &j.child_sort {
if let Some(o) = s.as_object() {
for (k, dir) in o {
let Some(c) = crate::dialect::scalar_column(rel_schema, k) else {
return Err(format!(
"$lookup 关系 {} 的子 $sort 字段 {} 无法映射到本层标量列,窗口函数下推不支持",
j.alias, k
));
};
let d = dir.as_i64().unwrap_or(1);
parts.push(format!(
"c.{} {}",
backend.pcol(&c),
if d >= 0 { "ASC" } else { "DESC" }
));
}
}
}
if parts.is_empty() {
parts.push(format!("c.{} ASC", q(backend, "_id")));
}
Ok(parts.join(", "))
}
#[derive(Debug, Clone)]
struct VectorSql {
col: String,
param_idx: usize,
limit: i64,
min_score: Option<f64>,
metric: String,
vector: Vec<f64>,
filter: Option<Value>,
}
fn parse_vector_search(schema: &Schema, arg: &Value) -> Result<VectorSql, String> {
let spec = crate::pipeline::vector::parse_stage(Some(schema), arg)?;
let col = crate::dialect::scalar_column(schema, &spec.field).ok_or_else(|| {
format!(
"vector 字段 {} 无法映射到 SQL 列(schema \"{}\")",
spec.field, schema.name
)
})?;
Ok(VectorSql {
col,
param_idx: 0,
limit: spec.limit,
min_score: spec.min_score,
metric: spec.metric,
vector: spec.vector,
filter: spec.filter,
})
}
fn vector_distance(backend: Backend, v: &VectorSql) -> String {
let op = match v.metric.as_str() {
"l2" => "<->",
"ip" => "<#>",
_ => "<=>",
};
format!(
"t.{} {} {}",
backend.pcol(&v.col),
op,
backend.placeholder(v.param_idx)
)
}
fn fold_score_min(m: &Value) -> Result<Option<f64>, String> {
let Some(obj) = m.as_object() else {
return Ok(None);
};
if !obj.contains_key("_score") {
return Ok(None);
}
if obj.len() != 1 {
return Err("向量评分条件必须是单键 {\"_score\": {\"$gte\": n}}".to_string());
}
let inner = obj
.get("_score")
.and_then(Value::as_object)
.ok_or_else(|| "向量评分条件 _score 必须是 {\"$gte\": n}".to_string())?;
if inner.len() != 1 {
return Err("向量评分条件 _score 仅支持 $gte".to_string());
}
inner
.get("$gte")
.and_then(Value::as_f64)
.map(Some)
.ok_or_else(|| "向量评分条件 _score.$gte 必须是数值".to_string())
}
pub(super) fn translate_aggregate(
backend: Backend,
schema: &Schema,
pipeline: &[Value],
registry: &Registry,
warnings: &mut Vec<String>,
unsupported: &mut Vec<Value>,
) -> Result<Vec<SqlStmt>, String> {
let mut param_seq = 0usize;
let mut root_matches: Vec<Value> = Vec::new();
let mut root_order: Vec<String> = Vec::new();
let mut root_limit: Option<i64> = None;
let mut root_offset: i64 = 0;
let mut joins: Vec<Join> = Vec::new();
let mut compute_aggs: Vec<ComputeAgg> = Vec::new();
let mut rel_preds: Vec<relation_agg::SqlRelPredicate> = Vec::new();
let mut project_on: Option<Vec<String>> = None;
let mut group: Option<GroupSpec> = None;
let mut group_having: Option<Value> = None;
let mut group_order: Vec<(String, i64)> = Vec::new();
let mut group_skip: i64 = 0;
let mut group_limit: Option<i64> = None;
let mut group_project: Option<Value> = None;
let mut vector: Option<VectorSql> = None;
for stage in pipeline {
if let Some(vs) = stage.get("$vectorSearch") {
if backend != Backend::Postgres {
return Err(format!(
"SQL 后端 {:?} 不支持原生向量检索:应由宿主 vector_scan 降级(拒绝静默全表扫描或忽略条件)",
backend
));
}
if vector.is_some() {
return Err("$vectorSearch 只允许出现一次(一次查询只做一次向量检索)".to_string());
}
if group.is_some()
|| project_on.is_some()
|| !joins.is_empty()
|| !compute_aggs.is_empty()
|| !root_matches.is_empty()
|| !root_order.is_empty()
|| root_limit.is_some()
{
return Err(
"$vectorSearch 必须是聚合管道首阶段(之前不得有 $match/$lookup/$sort/$limit/$project/$group)"
.to_string(),
);
}
let v = parse_vector_search(schema, vs)?;
if let Some(f) = &v.filter {
root_matches.push(f.clone());
}
root_limit = Some(v.limit);
vector = Some(VectorSql { filter: None, ..v });
} else if let Some(g) = stage.get("$group") {
group = Some(group_agg::parse_group(g)?);
} else if let Some(m) = stage.get("$match") {
if group.is_some() {
group_having = Some(m.clone());
} else if let Some(ms) = fold_score_min(m)? {
let v = vector.as_mut().ok_or_else(|| {
"评分条件 _score 只能与向量检索($near / $vectorSearch)并用".to_string()
})?;
if v.min_score.is_some() {
return Err("$near.$minScore 只允许出现一次".to_string());
}
v.min_score = Some(ms);
} else {
root_matches.push(m.clone());
}
} else if let Some(lo) = stage.get("$lookup") {
let as_name = lo.get("as").and_then(|v| v.as_str()).unwrap_or("");
if as_name.starts_with(REL_PRED_PREFIX) {
rel_preds.push(relation_agg::parse(lo, schema, registry, backend)?);
} else {
match resolve_compute_agg(schema, lo) {
Some(ca) => compute_aggs.push(ca),
None => {
collect_lookup(schema, registry, lo, None, &[], &[], &mut joins, warnings)?
}
}
}
} else if let Some(s) = stage.get("$sort") {
if let Some(o) = s.as_object() {
for (k, dir) in o {
if group.is_some() {
group_order.push((k.clone(), dir.as_i64().unwrap_or(1)));
continue;
}
let json_expr = |col: &str, path: &[String]| -> Result<String, String> {
let base = format!("t.{}", backend.pcol(col));
let segs: Vec<&str> = path.iter().map(|s| s.as_str()).collect();
backend.json_extract_scalar(&base, &segs)
};
let resolved: Option<String> = match k.split_once('.') {
Some((head, rest)) => {
if let Some(i) = joins.iter().position(|j| j.rel_name == head) {
registry
.get(&joins[i].model)
.ok()
.and_then(|rel| crate::dialect::scalar_column(rel, rest))
.map(|c| format!("r{}.{}", i, backend.pcol(&c)))
} else {
match crate::dialect::field_column_ref(schema, k) {
Some(ColumnRef::JsonPath(col, path)) => {
Some(json_expr(&col, &path)?)
}
_ => None,
}
}
}
None => {
if schema.relations.iter().any(|(n, _)| n.as_str() == k) {
None
} else {
match col_fn(schema)(k) {
Some(ColumnRef::Scalar(c)) => {
Some(format!("t.{}", backend.pcol(&c)))
}
Some(ColumnRef::Json(..)) => None,
Some(ColumnRef::JsonPath(col, path)) => {
Some(json_expr(&col, &path)?)
}
None => None,
}
}
}
};
match resolved {
Some(expr) => {
let d = dir.as_i64().unwrap_or(1);
root_order.push(format!(
"{} {}",
expr,
if d >= 0 { "ASC" } else { "DESC" }
));
}
None => {
warnings.push(format!(
"$sort 字段 {k} 无法映射到有效列(未知字段 / object·array 字段 / 无对应 $lookup),未下推排序"
));
unsupported.push(json!({
"code": "sortField",
"field": k,
"reason": "$sort 字段无法映射到有效列,需 Host 侧兜底排序",
}));
}
}
}
}
} else if let Some(l) = stage.get("$skip") {
if group.is_some() {
group_skip = l.as_i64().unwrap_or(0);
} else {
root_offset = l.as_i64().unwrap_or(0);
}
} else if let Some(l) = stage.get("$limit") {
if group.is_some() {
group_limit = l.as_i64();
} else {
root_limit = l.as_i64();
}
} else if let Some(p) = stage.get("$project") {
if group.is_some() {
group_project = Some(p.clone());
} else {
super::check_projection_supported(schema, Some(p))?;
project_on = Some(projection_fields(schema, Some(p)));
}
} else if stage.get("$facet").is_some() || stage.get("$replaceRoot").is_some() {
if group.is_none() {
let stage_name = if stage.get("$facet").is_some() {
"$facet"
} else {
"$replaceRoot"
};
return Err(format!("SQL 后端暂不支持的聚合阶段 {stage_name}"));
}
} else if let Some(u) = stage.get("$unwind") {
let path = u.get("path").and_then(|v| v.as_str()).unwrap_or("");
let rel = path.strip_prefix('$').unwrap_or(path);
if !joins
.iter()
.any(|j| j.path.len() == 1 && j.path[0] == rel && j.one)
{
return Err(format!(
"SQL 后端不支持的 $unwind 路径 {path}(非 one 关系 $lookup 产物)"
));
}
} else if let Some(af) = stage.get("$addFields") {
let is_score_only = af
.as_object()
.map(|o| {
o.len() == 1
&& o.get("_score")
.and_then(|v| v.get("$meta"))
.and_then(Value::as_str)
== Some("vectorSearchScore")
})
.unwrap_or(false);
if is_score_only && vector.is_some() {
continue;
}
let keys: Vec<String> = af
.as_object()
.map(|o| o.keys().cloned().collect())
.unwrap_or_default();
let all_resolved = !keys.is_empty()
&& keys
.iter()
.all(|k| compute_aggs.iter().any(|ca| &ca.key == k));
if !all_resolved {
return Err(
"SQL 后端暂不支持的聚合阶段 $addFields:拒绝静默忽略后返回未聚合的原始行"
.to_string(),
);
}
} else {
let stage_name = stage
.as_object()
.and_then(|m| m.keys().next())
.cloned()
.unwrap_or_else(|| "?".to_string());
return Err(format!(
"SQL 后端暂不支持的聚合阶段 {stage_name}:拒绝静默忽略后返回未聚合的原始行"
));
}
}
if let Some(spec) = group {
if vector.is_some() {
return Err(
"向量检索($vectorSearch)不能与 $group 并用(分组终结前无法保证 TopK 语义)"
.to_string(),
);
}
let stmt = group_agg::translate_group(
backend,
schema,
registry,
&spec,
&root_matches,
group_having.as_ref(),
&group_order,
group_skip,
group_limit,
group_project.as_ref(),
&mut param_seq,
warnings,
)?;
return Ok(vec![stmt]);
}
let selected = project_on.unwrap_or_else(|| schema.fields.keys().cloned().collect());
let mut cols_sql = Vec::new();
let mut columns: Vec<RowCol> = Vec::new();
cols_sql.push(format!("t.{}", q(backend, "_id")));
columns.push(RowCol::scalar("_id", &["_id"]));
let root_selected: Vec<String> = selected
.iter()
.filter(|f| schema.relations.iter().all(|(rn, _)| rn != *f))
.filter(|f| !compute_aggs.iter().any(|ca| &ca.key == *f))
.filter(|f| f.as_str() != "_score")
.cloned()
.collect();
for f in &root_selected {
match col_fn(schema)(f) {
Some(ColumnRef::Scalar(c)) => {
cols_sql.push(format!("t.{}", backend.pcol(&c)));
if field_is_json(schema, f) {
columns.push(RowCol::json(&physical_of(&c), &[f.as_str()]));
} else {
columns.push(RowCol::scalar_bool(
&physical_of(&c),
&[f.as_str()],
field_is_bool(schema, f),
));
}
}
Some(ColumnRef::Json(c, _)) => {
cols_sql.push(format!("t.{}", backend.pcol(&c)));
columns.push(RowCol::json(&physical_of(&c), &[f.as_str()]));
}
Some(ColumnRef::JsonPath(c, path)) => {
let base = format!("t.{}", backend.pcol(&c));
let segs: Vec<&str> = path.iter().map(|s| s.as_str()).collect();
let alias = f.replace('.', "_");
let expr = backend.json_extract_scalar(&base, &segs)?;
cols_sql.push(format!("{} AS {}", expr, q(backend, &alias)));
columns.push(RowCol::scalar(&alias, &f.split('.').collect::<Vec<_>>()));
}
None => {}
}
}
if let Some(v) = &vector {
if selected.iter().any(|f| f == "_score") {
let d = format!("({})", vector_distance(backend, v));
let score_expr = match v.metric.as_str() {
"l2" => format!("(1.0 / (1.0 + {}))", d),
"ip" => format!("(-{})", d),
_ => format!("(1.0 - {})", d),
};
cols_sql.push(format!("{} AS {}", score_expr, q(backend, "_score")));
columns.push(RowCol::computed("_score", &["_score"]));
}
}
if cols_sql.is_empty() && !selected.is_empty() {
cols_sql.push(format!("t.{}", q(backend, "_id")));
columns.push(RowCol::scalar("_id", &["_id"]));
}
let mut from_sql = format!("{} t", tname(backend, schema));
let mut all_params: Vec<Value> = Vec::new();
let mut join_params: Vec<Value> = Vec::new();
for (i, j) in joins.iter().enumerate() {
let r = format!("r{}", i);
let parent_alias = match j.parent {
Some(p) => format!("r{}", p),
None => "t".to_string(),
};
let rel_schema = match registry.get(&j.model) {
Ok(s) => s,
Err(e) => {
warnings.push(format!(
"$lookup 关系 {} 目标不可定位,跳过 JOIN: {}",
j.alias, e
));
continue;
}
};
let has_topn = j.child_limit.is_some() || j.child_skip.is_some();
let mut extra_sql = String::new();
if let Some(extra) = &j.extra {
let alias_for_extra = if has_topn { "c" } else { r.as_str() };
let wh = build_filter(
extra,
backend,
alias_for_extra,
&col_fn(rel_schema),
&mut param_seq,
Some(&mut *warnings),
)?;
if !wh.text.is_empty() {
extra_sql = wh.text;
join_params.extend(wh.params);
}
}
let child_ref = if has_topn {
let order = child_order_sql(backend, rel_schema, j)?;
let mut inner = format!(
"SELECT c.*, ROW_NUMBER() OVER (PARTITION BY c.{} ORDER BY {}) AS {} FROM {} c",
backend.pcol(&j.foreign_col),
order,
q(backend, "__rn"),
tname(backend, rel_schema),
);
if !extra_sql.is_empty() {
inner.push_str(&format!(" WHERE {}", extra_sql));
extra_sql.clear();
}
let mut conds: Vec<String> = Vec::new();
let skip = j.child_skip.unwrap_or(0);
if skip > 0 {
conds.push(format!("w.{} > {}", q(backend, "__rn"), skip));
}
if let Some(lm) = j.child_limit {
conds.push(format!("w.{} <= {}", q(backend, "__rn"), skip + lm));
}
if conds.is_empty() {
format!("({})", inner)
} else {
format!(
"(SELECT * FROM ({}) w WHERE {})",
inner,
conds.join(" AND ")
)
}
} else {
tname(backend, rel_schema)
};
let on_extra = if extra_sql.is_empty() {
String::new()
} else {
format!(" AND {}", extra_sql)
};
from_sql.push_str(&format!(
" LEFT JOIN {} {} ON {}.{} = {}.{}{}",
child_ref,
r,
r,
backend.pcol(&j.foreign_col),
parent_alias,
backend.pcol(&j.local_col),
on_extra,
));
let rel_cols = projection_fields(rel_schema, None);
for rf in rel_cols {
if rel_schema
.fields
.get(&rf)
.map(|f| f.field_type == "object" || f.field_type == "array")
.unwrap_or(false)
{
continue;
}
let alias_col = format!("{}_{}_{}", j.alias, i, rf);
cols_sql.push(format!(
"{}.{} AS {}",
r,
backend.pcol(&rf),
q(backend, &alias_col)
));
let mut json_path = j.path.clone();
json_path.push(rf.clone());
columns.push(RowCol {
alias: alias_col,
json_path,
is_array: true,
one: *j.ones.first().unwrap_or(&false),
ones: j.ones.clone(),
sub_shape: None,
always: false,
is_bool: field_is_bool(rel_schema, &rf),
is_json: field_is_json(rel_schema, &rf),
});
}
}
for (i, ca) in compute_aggs.iter().enumerate() {
let rel_schema = match registry.get(&ca.model) {
Ok(s) => s,
Err(e) => {
return Err(format!(
"计算列 \"{}\" 的聚合关系 \"{}\" 目标不可定位: {}",
ca.key, ca.rel_name, e
))
}
};
let alias = format!("a{}", i);
let mut extra_sql = String::new();
if let Some(extra) = &ca.extra {
let wh = build_filter(
extra,
backend,
"c",
&col_fn(rel_schema),
&mut param_seq,
Some(&mut *warnings),
)?;
if !wh.text.is_empty() {
extra_sql = wh.text;
join_params.extend(wh.params);
}
}
let fk = backend.pcol(&ca.foreign_col);
let agg_expr = match ca.op.as_str() {
"$count" => "COUNT(*)".to_string(),
op @ ("$sum" | "$avg" | "$min" | "$max") => {
let f = ca
.field
.as_deref()
.ok_or_else(|| format!("计算列 \"{}\" 的 {op} 必须引用关系字段", ca.key))?;
let col = crate::dialect::scalar_column(rel_schema, f).ok_or_else(|| {
format!(
"计算列 \"{}\" 的聚合字段 {} 无法映射到关系 {} 的标量列",
ca.key, f, ca.rel_name
)
})?;
let fn_name = match op {
"$sum" => "SUM",
"$avg" => "AVG",
"$min" => "MIN",
_ => "MAX",
};
let arg_sql = format!("c.{}", backend.pcol(&col));
if op == "$avg" {
format!("AVG(CAST({arg_sql} AS {}))", backend.double_type())
} else {
format!("{}({})", fn_name, arg_sql)
}
}
other => {
return Err(format!(
"计算列 \"{}\" 的 agg 算子 {other} 不在白名单($count/$sum/$avg/$min/$max)",
ca.key
))
}
};
let mut inner = format!(
"SELECT c.{} AS {}, {} AS {} FROM {} c",
fk,
q(backend, "fk"),
agg_expr,
q(backend, "v"),
tname(backend, rel_schema),
);
if !extra_sql.is_empty() {
inner.push_str(&format!(" WHERE {}", extra_sql));
}
inner.push_str(&format!(" GROUP BY c.{}", fk));
from_sql.push_str(&format!(
" LEFT JOIN ({}) {} ON {}.{} = t.{}",
inner,
alias,
alias,
q(backend, "fk"),
backend.pcol(&ca.local_col),
));
let out = if ca.op == "$count" {
format!("COALESCE({}.{}, 0)", alias, q(backend, "v"))
} else {
format!("{}.{}", alias, q(backend, "v"))
};
cols_sql.push(format!("{} AS {}", out, q(backend, &ca.key)));
columns.push(RowCol::computed(&ca.key, &[ca.key.as_str()]));
}
let resolver = PredResolver {
preds: &rel_preds,
backend,
schema,
registry,
};
if let Some(v) = vector.as_mut() {
v.param_idx = param_seq;
param_seq += 1;
}
let mut root_wheres: Vec<WhereClause> = Vec::new();
for m in &root_matches {
let wh = build_filter_with_relations(
m,
backend,
"t",
&col_fn(schema),
Some(&resolver),
&mut param_seq,
Some(&mut *warnings),
)?;
if !wh.text.is_empty() {
root_wheres.push(wh);
}
}
if let Some(v) = &vector {
let dist = vector_distance(backend, v);
if let Some(ms) = v.min_score {
let bound = match v.metric.as_str() {
"l2" if ms > 0.0 => Some(1.0 / ms - 1.0),
"l2" => None,
"ip" => Some(-ms),
_ => Some(1.0 - ms),
};
if let Some(b) = bound {
let ph = backend.placeholder(param_seq);
param_seq += 1;
root_wheres.push(WhereClause {
text: format!("{} <= {}", dist, ph),
params: vec![json!(b)],
});
}
}
root_order.push(format!("{} ASC", dist));
}
all_params.extend(join_params);
if let Some(v) = &vector {
all_params.push(json!(v.vector));
}
let where_sql = if root_wheres.is_empty() {
String::new()
} else {
let text = root_wheres
.iter()
.map(|w| w.text.clone())
.collect::<Vec<_>>()
.join(" AND ");
for w in &root_wheres {
all_params.extend(w.params.clone());
}
format!(" WHERE {}", text)
};
let order_sql = if root_order.is_empty() {
String::new()
} else {
format!(" ORDER BY {}", root_order.join(", "))
};
let (limit_sql, limit_params) =
limit_offset_sql(backend, root_limit, root_offset, &mut param_seq);
all_params.extend(limit_params);
let select_list = if cols_sql.is_empty() {
q(backend, "_id")
} else if !cols_sql.iter().any(|c| c.contains("__present")) {
cols_sql.push("t.__present AS __present".to_string());
cols_sql.join(", ")
} else {
cols_sql.join(", ")
};
let text = format!(
"SELECT {} FROM {}{}{}{}",
select_list, from_sql, where_sql, order_sql, limit_sql,
);
Ok(vec![SqlStmt::select(
text,
all_params,
RowShape {
columns,
present_alias: Some("__present".to_string()),
},
)])
}