use std::cell::RefCell;
use std::collections::HashMap;
use helix_core::effect::UpsertSpec;
#[derive(PartialEq, Eq, Hash)]
struct UpsertShapeKey {
table: &'static str,
conflict_key: Option<&'static str>,
col_names: Vec<String>,
exclude_from_update: Vec<&'static str>,
}
thread_local! {
static UPSERT_SQL_CACHE: RefCell<HashMap<UpsertShapeKey, String>> =
RefCell::new(HashMap::new());
}
pub fn upsert_sql_for_shape(spec: &UpsertSpec) -> String {
let col_names: Vec<String> = spec.rows[0].iter().map(|(k, _)| k.clone()).collect();
let key = UpsertShapeKey {
table: spec.table,
conflict_key: spec.conflict_key,
col_names,
exclude_from_update: spec.exclude_from_update.clone(),
};
UPSERT_SQL_CACHE.with(|cache| {
if let Some(sql) = cache.borrow().get(&key) {
return sql.clone();
}
let sql = build_upsert_sql(&key);
cache.borrow_mut().insert(key, sql.clone());
sql
})
}
fn build_upsert_sql(key: &UpsertShapeKey) -> String {
let cols_sql = key.col_names.join(", ");
let placeholders = key
.col_names
.iter()
.map(|_| "?")
.collect::<Vec<_>>()
.join(", ");
match key.conflict_key {
Some(conflict_key) => {
let update_cols: Vec<String> = key
.col_names
.iter()
.filter(|c| {
c.as_str() != conflict_key && !key.exclude_from_update.contains(&c.as_str())
})
.map(|c| format!("{c} = excluded.{c}"))
.collect();
if update_cols.is_empty() {
format!(
"INSERT INTO {table} ({cols}) VALUES ({placeholders}) ON CONFLICT({conflict_key}) DO NOTHING",
table = key.table,
cols = cols_sql,
)
} else {
format!(
"INSERT INTO {table} ({cols}) VALUES ({placeholders}) ON CONFLICT({conflict_key}) DO UPDATE SET {updates}",
table = key.table,
cols = cols_sql,
updates = update_cols.join(", "),
)
}
}
None => format!(
"INSERT OR REPLACE INTO {table} ({cols}) VALUES ({placeholders})",
table = key.table,
cols = cols_sql,
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use helix_core::effect::{Row, SqlValue};
fn row(cols: &[(&str, i64)]) -> Row {
cols.iter()
.map(|(k, v)| (k.to_string(), SqlValue::Integer(*v)))
.collect()
}
fn spec(
table: &'static str,
cols: &[(&str, i64)],
conflict_key: Option<&'static str>,
exclude: Vec<&'static str>,
) -> UpsertSpec {
UpsertSpec {
table,
rows: vec![row(cols)],
conflict_key,
exclude_from_update: exclude,
}
}
#[test]
fn same_shape_reuses_cached_sql() {
let s1 = spec("message", &[("id", 1), ("content", 0)], Some("id"), vec![]);
let s2 = spec("message", &[("id", 9), ("content", 0)], Some("id"), vec![]);
let sql1 = upsert_sql_for_shape(&s1);
let sql2 = upsert_sql_for_shape(&s2);
assert_eq!(
sql1, sql2,
"同 (table, 列名集, conflict_key, exclude) 形状须产出逐字相同 SQL"
);
assert_eq!(
sql1,
"INSERT INTO message (id, content) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET content = excluded.content",
);
}
#[test]
fn distinct_shapes_each_correct() {
let other_table = upsert_sql_for_shape(&spec("evt", &[("id", 1)], Some("id"), vec![]));
assert_eq!(
other_table, "INSERT INTO evt (id) VALUES (?) ON CONFLICT(id) DO NOTHING",
"单列即 conflict_key → DO NOTHING(无可更新列)"
);
let no_conflict = upsert_sql_for_shape(&spec("kv", &[("k", 1), ("v", 2)], None, vec![]));
assert_eq!(
no_conflict,
"INSERT OR REPLACE INTO kv (k, v) VALUES (?, ?)",
);
let with_exclude = upsert_sql_for_shape(&spec(
"ch",
&[("id", 1), ("name", 0), ("local_flag", 0)],
Some("id"),
vec!["local_flag"],
));
assert_eq!(
with_exclude,
"INSERT INTO ch (id, name, local_flag) VALUES (?, ?, ?) ON CONFLICT(id) DO UPDATE SET name = excluded.name",
"exclude 列出现在 INSERT 列集,但被 DO UPDATE SET 排除"
);
}
}