use serde_json::{json, Value};
use crate::schema::{Registry, Schema};
use crate::dialect::filter::build_filter;
use crate::dialect::ir::{RowShape, SqlStmt};
use crate::dialect::{Backend, ColumnRef};
mod aggregate;
mod find;
mod group_agg;
mod lookup_join;
mod relation_agg;
use aggregate::translate_aggregate;
use find::translate_find;
pub fn translate_select(
backend: Backend,
cmd: &Value,
registry: &Registry,
warnings: &mut Vec<String>,
unsupported: &mut Vec<Value>,
) -> Result<Vec<SqlStmt>, String> {
let kind = cmd.get("kind").and_then(|v| v.as_str()).unwrap_or("");
let collection = cmd.get("collection").and_then(|v| v.as_str()).unwrap_or("");
let source = cmd
.get("source")
.and_then(|v| v.as_str())
.unwrap_or(crate::datasource::DEFAULT_SOURCE);
let database = cmd.get("database").and_then(|v| v.as_str());
let schema_q = cmd.get("schema").and_then(|v| v.as_str());
let mut schema = registry
.get_for_command(source, database, schema_q, collection)?
.clone();
schema.database = database.filter(|s| !s.is_empty()).map(String::from);
schema.schema = schema_q.filter(|s| !s.is_empty()).map(String::from);
let schema = &schema;
match kind {
"countDocuments" => {
let filter = cmd.get("filter").cloned().unwrap_or(json!({}));
let mut seq = 0usize;
let wh = build_filter(
&filter,
backend,
"t",
&col_fn(schema),
&mut seq,
Some(warnings),
)?;
let where_sql = if wh.text.is_empty() {
String::new()
} else {
format!(" WHERE {}", wh.text)
};
let text = format!(
"SELECT COUNT(*) FROM {} t{}",
tname(backend, schema),
where_sql
);
Ok(vec![SqlStmt::select(text, wh.params, RowShape::empty())])
}
"find" | "findOne" => {
let filter = cmd.get("filter").cloned().unwrap_or(json!({}));
let projection = cmd.get("projection").cloned();
translate_find(
backend,
schema,
&filter,
projection.as_ref(),
Some(warnings),
)
}
"aggregate" => {
let pipeline = cmd
.get("pipeline")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
translate_aggregate(backend, schema, &pipeline, registry, warnings, unsupported)
}
_ => Err(format!("translate: 未知命令 kind = {}", kind)),
}
}
pub(in crate::dialect::select) fn col_fn(
schema: &Schema,
) -> impl Fn(&str) -> Option<ColumnRef> + '_ {
move |field: &str| super::field_column_ref(schema, field)
}
pub(in crate::dialect::select) fn projection_fields(
schema: &Schema,
projection: Option<&Value>,
) -> Vec<String> {
match projection {
None | Some(Value::Null) => schema.fields.keys().cloned().collect(),
Some(p) => p
.as_object()
.map(|o| {
let on: Vec<String> = o
.iter()
.filter(|(_, v)| {
v.as_i64().map(|n| n != 0).unwrap_or(false) || v.as_bool() == Some(true)
})
.map(|(k, _)| k.clone())
.collect();
if on.is_empty() {
schema.fields.keys().cloned().collect()
} else {
on
}
})
.unwrap_or_else(|| schema.fields.keys().cloned().collect()),
}
}
pub(super) fn count_field_pattern(v: &Value) -> Option<String> {
let arr = v.get("$cond")?.as_array()?;
if arr.len() != 3 {
return None;
}
let cond = arr[0].get("$eq")?.as_array()?;
let if_null = cond.first()?.get("$ifNull")?.as_array()?;
let f = if_null.first()?.as_str()?;
if cond.get(1)?.is_null() && arr[1].as_i64()? == 0 && arr[2].as_i64()? == 1 {
return f.strip_prefix('$').map(|x| x.to_string());
}
None
}
pub(in crate::dialect::select) fn check_projection_supported(
schema: &Schema,
projection: Option<&Value>,
) -> Result<(), String> {
let Some(o) = projection.and_then(|p| p.as_object()) else {
return Ok(());
};
for (k, v) in o {
let requested = v.as_i64().map(|n| n != 0).unwrap_or(false) || v.as_bool() == Some(true);
if !requested || k == "_id" {
continue;
}
if schema.relations.contains_key(k.as_str()) || schema.compute(k).is_some() {
continue;
}
if super::field_column_ref(schema, k).is_none() {
return Err(format!(
"SQL 后端不支持投影字段 \"{k}\"(未声明字段;D2:绝不静默返回残缺结果)"
));
}
}
Ok(())
}
pub(in crate::dialect::select) fn q(backend: Backend, ident: &str) -> String {
backend.quote_ident(ident)
}
pub(in crate::dialect::select) fn tname(backend: Backend, schema: &Schema) -> String {
backend.qualified_table(schema.database(), schema.schema(), &schema.collection)
}
pub(in crate::dialect::select) fn limit_offset_sql(
backend: Backend,
limit: Option<i64>,
offset: i64,
param_seq: &mut usize,
) -> (String, Vec<Value>) {
let mut sql = String::new();
let mut params: Vec<Value> = Vec::new();
if let Some(lim) = limit {
match backend {
Backend::Postgres => {
sql = format!(" LIMIT ${}", *param_seq + 1);
*param_seq += 1;
params.push(json!(lim));
if offset > 0 {
sql.push_str(&format!(" OFFSET ${}", *param_seq + 1));
params.push(json!(offset));
}
}
_ => {
if offset > 0 {
sql = " LIMIT ? OFFSET ?".to_string();
params.push(json!(lim));
params.push(json!(offset));
} else {
sql = " LIMIT ?".to_string();
params.push(json!(lim));
}
}
}
} else if offset > 0 {
match backend {
Backend::Postgres => {
sql = format!(" OFFSET ${}", *param_seq + 1);
params.push(json!(offset));
}
Backend::Mysql => {
sql = " LIMIT 18446744073709551615 OFFSET ?".to_string();
params.push(json!(offset));
}
Backend::Sqlite => {
sql = " LIMIT -1 OFFSET ?".to_string();
params.push(json!(offset));
}
}
}
(sql, params)
}