use serde_json::{json, Value};
use crate::dialect::ir::SqlStmt;
use crate::dialect::Backend;
use crate::schema::Schema;
use super::update::{build_assignments, read_back, write_with_returning};
use super::{bind_value, returning_cols, scalar_col, tname, writable_col, Binder};
pub(super) fn translate_upsert(
backend: Backend,
schema: &Schema,
filter: &Value,
update: &Value,
) -> Result<Vec<SqlStmt>, String> {
let targets = upsert_target_pairs(schema, filter)?;
let target: Vec<String> = targets.iter().map(|(c, _)| c.clone()).collect();
let mut cols: Vec<String> = Vec::new();
let mut vals: Vec<Value> = Vec::new();
for (col, val) in &targets {
if cols.iter().any(|c| c == col) {
continue;
}
cols.push(col.clone());
vals.push(val.clone());
}
for key in ["$setOnInsert", "$set"] {
let Some(o) = update.get(key).and_then(|v| v.as_object()) else {
continue;
};
for (k, v) in o {
let Some(col) = writable_col(schema, k) else {
continue;
};
if v.is_null() || cols.iter().any(|c| c == &col) {
continue;
}
let bound = bind_value(schema, &col, v);
cols.push(col);
vals.push(bound);
}
}
if cols.is_empty() {
return Err("upsert 无可写标量字段".to_string());
}
let mut binder = Binder::new(backend);
let cols_sql = cols
.iter()
.map(|c| backend.pcol(c))
.collect::<Vec<_>>()
.join(", ");
let phs: Vec<String> = vals.iter().map(|v| binder.bind(v.clone())).collect();
let present_qualifier = if backend == Backend::Postgres {
Some(tname(backend, schema))
} else {
None
};
let set_update = json!({ "$set": update.get("$set").cloned().unwrap_or(json!({})) });
let assigns = build_assignments(
&mut binder,
schema,
&set_update,
present_qualifier.as_deref(),
)?;
if assigns.is_empty() {
return Err("upsert 无冲突更新字段".to_string());
}
let returning = returning_cols(schema);
let insert_text = format!(
"INSERT INTO {} ({}) VALUES ({})",
tname(backend, schema),
cols_sql,
phs.join(", "),
);
Ok(if backend.supports_returning() {
let target_sql = target
.iter()
.map(|c| backend.pcol(c))
.collect::<Vec<_>>()
.join(", ");
let text = format!(
"{} ON CONFLICT ({}) DO UPDATE SET {}",
insert_text,
target_sql,
assigns.join(", "),
);
vec![write_with_returning(
backend,
schema,
text,
&returning,
binder.params,
)]
} else {
let text = format!(
"{} ON DUPLICATE KEY UPDATE {}",
insert_text,
assigns.join(", "),
);
vec![
SqlStmt::write(text, binder.params),
read_back(backend, schema, filter, &returning)?,
]
})
}
fn upsert_target_pairs(schema: &Schema, filter: &Value) -> Result<Vec<(String, Value)>, String> {
let cond = match filter.as_object() {
Some(o) => {
if let Some(first) = o
.get("$or")
.and_then(|v| v.as_array())
.and_then(|a| a.first())
{
first.as_object().cloned().unwrap_or_default()
} else {
o.clone()
}
}
None => return Err("upsert 需要条件对象".to_string()),
};
let mut pairs: Vec<(String, Value)> = Vec::new();
for (k, v) in cond.iter() {
if k.starts_with('$') {
continue;
}
let Some(col) = scalar_col(schema, k) else {
continue;
};
if v.is_object() {
return Err(format!("upsert 条件 {} 需为等值(唯一键)条件", k));
}
if !pairs.iter().any(|(c, _)| c == &col) {
pairs.push((col, v.clone()));
}
}
if pairs.is_empty() {
return Err("upsert 需要唯一键条件(_id 或 unique 索引字段)".to_string());
}
Ok(pairs)
}