use std::collections::HashMap;
use sqlx::{Column, Row};
use crate::admin::form::FormConfig;
use crate::error::Error;
use crate::orm::Db;
pub async fn ensure_table(db: &Db, sql: &str) -> Result<(), Error> {
db.execute(sql).await
}
pub fn form_to_column_map(form: &FormConfig, primary_key: &str) -> HashMap<String, String> {
let mut m = HashMap::new();
for f in &form.fields {
if f.name == primary_key {
continue;
}
m.insert(f.name.clone(), f.value.clone().unwrap_or_default());
}
m
}
pub async fn insert_record(
db: &Db,
table: &str,
data: &HashMap<String, String>,
) -> Result<i64, Error> {
if data.is_empty() {
return Err(Error::Internal("insert_record: no columns supplied".into()));
}
let mut cols: Vec<&String> = data.keys().collect();
cols.sort();
let cols_sql = cols
.iter()
.map(|c| quote_ident(c))
.collect::<Vec<_>>()
.join(", ");
let placeholders = vec!["?"; cols.len()].join(", ");
let sql = format!(
"INSERT INTO {} ({}) VALUES ({})",
quote_ident(table),
cols_sql,
placeholders,
);
let mut q = sqlx::query(&sql);
for col in &cols {
q = q.bind(data.get(*col).map(String::as_str).unwrap_or(""));
}
let result = q.execute(db.pool()).await.map_err(Error::from)?;
Ok(result.last_insert_rowid())
}
pub async fn update_record(
db: &Db,
table: &str,
id: &str,
data: &HashMap<String, String>,
) -> Result<(), Error> {
if data.is_empty() {
return Err(Error::Internal("update_record: no columns supplied".into()));
}
let mut cols: Vec<&String> = data.keys().collect();
cols.sort();
let set_clause = cols
.iter()
.map(|c| format!("{} = ?", quote_ident(c)))
.collect::<Vec<_>>()
.join(", ");
let sql = format!(
"UPDATE {} SET {} WHERE \"id\" = ?",
quote_ident(table),
set_clause,
);
let mut q = sqlx::query(&sql);
for col in &cols {
q = q.bind(data.get(*col).map(String::as_str).unwrap_or(""));
}
q = q.bind(id);
q.execute(db.pool()).await.map_err(Error::from)?;
Ok(())
}
fn quote_ident(s: &str) -> String {
format!("\"{}\"", s.replace('"', "\"\""))
}
pub async fn get_record_by_id(
db: &Db,
table: &str,
id: &str,
) -> Result<HashMap<String, String>, Error> {
let sql = format!("SELECT * FROM {} WHERE \"id\" = ?", quote_ident(table));
let row_opt = sqlx::query(&sql)
.bind(id)
.fetch_optional(db.pool())
.await
.map_err(Error::from)?;
let row = match row_opt {
Some(r) => r,
None => return Ok(HashMap::new()),
};
Ok(row_to_map(&row))
}
pub async fn list_records(
db: &Db,
table: &str,
limit: i64,
offset: i64,
) -> Result<Vec<HashMap<String, String>>, Error> {
let sql = format!(
"SELECT * FROM {} ORDER BY \"id\" DESC LIMIT ? OFFSET ?",
quote_ident(table),
);
let rows = sqlx::query(&sql)
.bind(limit)
.bind(offset)
.fetch_all(db.pool())
.await
.map_err(Error::from)?;
Ok(rows.iter().map(row_to_map).collect())
}
pub async fn count_records(db: &Db, table: &str) -> Result<i64, Error> {
let sql = format!("SELECT COUNT(*) FROM {}", quote_ident(table));
let count: i64 = sqlx::query_scalar(&sql)
.fetch_one(db.pool())
.await
.map_err(Error::from)?;
Ok(count)
}
pub async fn search_records(
db: &Db,
table: &str,
searchable_fields: &[&str],
query: &str,
limit: i64,
offset: i64,
) -> Result<Vec<HashMap<String, String>>, Error> {
if searchable_fields.is_empty() {
return list_records(db, table, limit, offset).await;
}
let q = format!("%{}%", query.to_lowercase());
let where_sql = build_search_where(searchable_fields);
let sql = format!(
"SELECT * FROM {t} WHERE {where_sql} ORDER BY \"id\" DESC LIMIT ? OFFSET ?",
t = quote_ident(table),
);
let mut stmt = sqlx::query(&sql);
for _ in searchable_fields {
stmt = stmt.bind(&q);
}
stmt = stmt.bind(limit).bind(offset);
let rows = stmt.fetch_all(db.pool()).await.map_err(Error::from)?;
Ok(rows.iter().map(row_to_map).collect())
}
pub async fn count_search_records(
db: &Db,
table: &str,
searchable_fields: &[&str],
query: &str,
) -> Result<i64, Error> {
if searchable_fields.is_empty() {
return count_records(db, table).await;
}
let q = format!("%{}%", query.to_lowercase());
let where_sql = build_search_where(searchable_fields);
let sql = format!(
"SELECT COUNT(*) FROM {t} WHERE {where_sql}",
t = quote_ident(table),
);
let mut stmt = sqlx::query_scalar::<_, i64>(&sql);
for _ in searchable_fields {
stmt = stmt.bind(&q);
}
let count = stmt.fetch_one(db.pool()).await.map_err(Error::from)?;
Ok(count)
}
fn build_search_where(searchable_fields: &[&str]) -> String {
searchable_fields
.iter()
.map(|f| format!("LOWER({}) LIKE ?", quote_ident(f)))
.collect::<Vec<_>>()
.join(" OR ")
}
#[allow(clippy::too_many_arguments)]
pub async fn filter_records(
db: &Db,
table: &str,
eq_filters: &HashMap<String, String>,
like_filters: &HashMap<String, String>,
query: Option<&str>,
searchable_fields: &[&str],
sort: Option<&str>,
dir: Option<&str>,
limit: i64,
offset: i64,
) -> Result<Vec<HashMap<String, String>>, Error> {
let (where_sql, binds) = build_filter_where(eq_filters, like_filters, query, searchable_fields);
let order_sql = match sort {
Some(col) => {
let direction = if matches!(dir, Some("desc")) {
"DESC"
} else {
"ASC"
};
format!("ORDER BY {} {}", quote_ident(col), direction)
}
None => "ORDER BY \"id\" DESC".to_string(),
};
let sql = format!(
"SELECT * FROM {t} WHERE {where_sql} {order_sql} LIMIT ? OFFSET ?",
t = quote_ident(table),
);
let mut q = sqlx::query(&sql);
for b in &binds {
q = q.bind(b.as_str());
}
q = q.bind(limit).bind(offset);
let rows = q.fetch_all(db.pool()).await.map_err(Error::from)?;
Ok(rows.iter().map(row_to_map).collect())
}
pub async fn count_filtered_records(
db: &Db,
table: &str,
eq_filters: &HashMap<String, String>,
like_filters: &HashMap<String, String>,
query: Option<&str>,
searchable_fields: &[&str],
) -> Result<i64, Error> {
let (where_sql, binds) = build_filter_where(eq_filters, like_filters, query, searchable_fields);
let sql = format!(
"SELECT COUNT(*) FROM {t} WHERE {where_sql}",
t = quote_ident(table),
);
let mut q = sqlx::query_scalar::<_, i64>(&sql);
for b in &binds {
q = q.bind(b.as_str());
}
let count = q.fetch_one(db.pool()).await.map_err(Error::from)?;
Ok(count)
}
pub async fn bulk_update(
db: &Db,
table: &str,
ids: &[String],
field: &str,
value: &str,
) -> Result<(), Error> {
if ids.is_empty() {
return Ok(());
}
let placeholders = vec!["?"; ids.len()].join(", ");
let sql = format!(
"UPDATE {} SET {} = ? WHERE \"id\" IN ({})",
quote_ident(table),
quote_ident(field),
placeholders,
);
let mut q = sqlx::query(&sql);
q = q.bind(value);
for id in ids {
q = q.bind(id.as_str());
}
q.execute(db.pool()).await.map_err(Error::from)?;
Ok(())
}
pub async fn bulk_delete(db: &Db, table: &str, ids: &[String]) -> Result<(), Error> {
if ids.is_empty() {
return Ok(());
}
let placeholders = vec!["?"; ids.len()].join(", ");
let sql = format!(
"DELETE FROM {} WHERE \"id\" IN ({})",
quote_ident(table),
placeholders,
);
let mut q = sqlx::query(&sql);
for id in ids {
q = q.bind(id.as_str());
}
q.execute(db.pool()).await.map_err(Error::from)?;
Ok(())
}
fn build_filter_where(
eq_filters: &HashMap<String, String>,
like_filters: &HashMap<String, String>,
query: Option<&str>,
searchable_fields: &[&str],
) -> (String, Vec<String>) {
let mut clauses: Vec<String> = vec!["1=1".to_string()];
let mut binds: Vec<String> = Vec::new();
let mut eq_keys: Vec<&String> = eq_filters.keys().collect();
eq_keys.sort();
for k in eq_keys {
clauses.push(format!("{} = ?", quote_ident(k)));
binds.push(eq_filters.get(k).cloned().unwrap_or_default());
}
let mut like_keys: Vec<&String> = like_filters.keys().collect();
like_keys.sort();
for k in like_keys {
clauses.push(format!("LOWER({}) LIKE ?", quote_ident(k)));
let v = like_filters
.get(k)
.map(|s| s.to_lowercase())
.unwrap_or_default();
binds.push(format!("%{v}%"));
}
let trimmed_query = query.map(str::trim).filter(|s| !s.is_empty());
if let Some(q) = trimmed_query {
if !searchable_fields.is_empty() {
let or_body = build_search_where(searchable_fields);
clauses.push(format!("({or_body})"));
let q_param = format!("%{}%", q.to_lowercase());
for _ in searchable_fields {
binds.push(q_param.clone());
}
}
}
(clauses.join(" AND "), binds)
}
fn row_to_map(row: &sqlx::sqlite::SqliteRow) -> HashMap<String, String> {
let mut out = HashMap::new();
for col in row.columns() {
let name = col.name();
let value: String = if let Ok(Some(s)) = row.try_get::<Option<String>, _>(name) {
s
} else if let Ok(Some(n)) = row.try_get::<Option<i64>, _>(name) {
n.to_string()
} else if let Ok(Some(f)) = row.try_get::<Option<f64>, _>(name) {
f.to_string()
} else {
String::new()
};
out.insert(name.to_string(), value);
}
out
}