1use std::collections::HashMap;
10
11use sqlx::{Column, Row};
12
13use crate::admin::form::FormConfig;
14use crate::error::Error;
15use crate::orm::Db;
16
17pub async fn ensure_table(db: &Db, sql: &str) -> Result<(), Error> {
21 db.execute(sql).await
22}
23
24pub fn form_to_column_map(form: &FormConfig, primary_key: &str) -> HashMap<String, String> {
29 let mut m = HashMap::new();
30 for f in &form.fields {
31 if f.name == primary_key {
32 continue;
33 }
34 m.insert(f.name.clone(), f.value.clone().unwrap_or_default());
35 }
36 m
37}
38
39pub async fn insert_record(
46 db: &Db,
47 table: &str,
48 data: &HashMap<String, String>,
49) -> Result<i64, Error> {
50 if data.is_empty() {
51 return Err(Error::Internal("insert_record: no columns supplied".into()));
52 }
53 let mut cols: Vec<&String> = data.keys().collect();
54 cols.sort();
55
56 let cols_sql = cols
57 .iter()
58 .map(|c| quote_ident(c))
59 .collect::<Vec<_>>()
60 .join(", ");
61 let placeholders = vec!["?"; cols.len()].join(", ");
62
63 let sql = format!(
64 "INSERT INTO {} ({}) VALUES ({})",
65 quote_ident(table),
66 cols_sql,
67 placeholders,
68 );
69
70 let mut q = sqlx::query(&sql);
71 for col in &cols {
72 q = q.bind(data.get(*col).map(String::as_str).unwrap_or(""));
73 }
74 let result = q.execute(db.pool()).await.map_err(Error::from)?;
75 Ok(result.last_insert_rowid())
76}
77
78pub async fn update_record(
82 db: &Db,
83 table: &str,
84 id: &str,
85 data: &HashMap<String, String>,
86) -> Result<(), Error> {
87 if data.is_empty() {
88 return Err(Error::Internal("update_record: no columns supplied".into()));
89 }
90 let mut cols: Vec<&String> = data.keys().collect();
91 cols.sort();
92
93 let set_clause = cols
94 .iter()
95 .map(|c| format!("{} = ?", quote_ident(c)))
96 .collect::<Vec<_>>()
97 .join(", ");
98
99 let sql = format!(
100 "UPDATE {} SET {} WHERE \"id\" = ?",
101 quote_ident(table),
102 set_clause,
103 );
104
105 let mut q = sqlx::query(&sql);
106 for col in &cols {
107 q = q.bind(data.get(*col).map(String::as_str).unwrap_or(""));
108 }
109 q = q.bind(id);
110 q.execute(db.pool()).await.map_err(Error::from)?;
111 Ok(())
112}
113
114fn quote_ident(s: &str) -> String {
119 format!("\"{}\"", s.replace('"', "\"\""))
120}
121
122pub async fn get_record_by_id(
135 db: &Db,
136 table: &str,
137 id: &str,
138) -> Result<HashMap<String, String>, Error> {
139 let sql = format!("SELECT * FROM {} WHERE \"id\" = ?", quote_ident(table));
140 let row_opt = sqlx::query(&sql)
141 .bind(id)
142 .fetch_optional(db.pool())
143 .await
144 .map_err(Error::from)?;
145
146 let row = match row_opt {
147 Some(r) => r,
148 None => return Ok(HashMap::new()),
149 };
150
151 Ok(row_to_map(&row))
152}
153
154pub async fn list_records(
163 db: &Db,
164 table: &str,
165 limit: i64,
166 offset: i64,
167) -> Result<Vec<HashMap<String, String>>, Error> {
168 let sql = format!(
169 "SELECT * FROM {} ORDER BY \"id\" DESC LIMIT ? OFFSET ?",
170 quote_ident(table),
171 );
172 let rows = sqlx::query(&sql)
173 .bind(limit)
174 .bind(offset)
175 .fetch_all(db.pool())
176 .await
177 .map_err(Error::from)?;
178
179 Ok(rows.iter().map(row_to_map).collect())
180}
181
182pub async fn count_records(db: &Db, table: &str) -> Result<i64, Error> {
186 let sql = format!("SELECT COUNT(*) FROM {}", quote_ident(table));
187 let count: i64 = sqlx::query_scalar(&sql)
188 .fetch_one(db.pool())
189 .await
190 .map_err(Error::from)?;
191 Ok(count)
192}
193
194pub async fn search_records(
202 db: &Db,
203 table: &str,
204 searchable_fields: &[&str],
205 query: &str,
206 limit: i64,
207 offset: i64,
208) -> Result<Vec<HashMap<String, String>>, Error> {
209 if searchable_fields.is_empty() {
210 return list_records(db, table, limit, offset).await;
211 }
212 let q = format!("%{}%", query.to_lowercase());
213 let where_sql = build_search_where(searchable_fields);
214 let sql = format!(
215 "SELECT * FROM {t} WHERE {where_sql} ORDER BY \"id\" DESC LIMIT ? OFFSET ?",
216 t = quote_ident(table),
217 );
218 let mut stmt = sqlx::query(&sql);
219 for _ in searchable_fields {
220 stmt = stmt.bind(&q);
221 }
222 stmt = stmt.bind(limit).bind(offset);
223 let rows = stmt.fetch_all(db.pool()).await.map_err(Error::from)?;
224 Ok(rows.iter().map(row_to_map).collect())
225}
226
227pub async fn count_search_records(
231 db: &Db,
232 table: &str,
233 searchable_fields: &[&str],
234 query: &str,
235) -> Result<i64, Error> {
236 if searchable_fields.is_empty() {
237 return count_records(db, table).await;
238 }
239 let q = format!("%{}%", query.to_lowercase());
240 let where_sql = build_search_where(searchable_fields);
241 let sql = format!(
242 "SELECT COUNT(*) FROM {t} WHERE {where_sql}",
243 t = quote_ident(table),
244 );
245 let mut stmt = sqlx::query_scalar::<_, i64>(&sql);
246 for _ in searchable_fields {
247 stmt = stmt.bind(&q);
248 }
249 let count = stmt.fetch_one(db.pool()).await.map_err(Error::from)?;
250 Ok(count)
251}
252
253fn build_search_where(searchable_fields: &[&str]) -> String {
257 searchable_fields
258 .iter()
259 .map(|f| format!("LOWER({}) LIKE ?", quote_ident(f)))
260 .collect::<Vec<_>>()
261 .join(" OR ")
262}
263
264#[allow(clippy::too_many_arguments)]
284pub async fn filter_records(
285 db: &Db,
286 table: &str,
287 eq_filters: &HashMap<String, String>,
288 like_filters: &HashMap<String, String>,
289 query: Option<&str>,
290 searchable_fields: &[&str],
291 sort: Option<&str>,
292 dir: Option<&str>,
293 limit: i64,
294 offset: i64,
295) -> Result<Vec<HashMap<String, String>>, Error> {
296 let (where_sql, binds) = build_filter_where(eq_filters, like_filters, query, searchable_fields);
297 let order_sql = match sort {
302 Some(col) => {
303 let direction = if matches!(dir, Some("desc")) {
304 "DESC"
305 } else {
306 "ASC"
307 };
308 format!("ORDER BY {} {}", quote_ident(col), direction)
309 }
310 None => "ORDER BY \"id\" DESC".to_string(),
311 };
312 let sql = format!(
313 "SELECT * FROM {t} WHERE {where_sql} {order_sql} LIMIT ? OFFSET ?",
314 t = quote_ident(table),
315 );
316 let mut q = sqlx::query(&sql);
317 for b in &binds {
318 q = q.bind(b.as_str());
319 }
320 q = q.bind(limit).bind(offset);
321 let rows = q.fetch_all(db.pool()).await.map_err(Error::from)?;
322 Ok(rows.iter().map(row_to_map).collect())
323}
324
325pub async fn count_filtered_records(
329 db: &Db,
330 table: &str,
331 eq_filters: &HashMap<String, String>,
332 like_filters: &HashMap<String, String>,
333 query: Option<&str>,
334 searchable_fields: &[&str],
335) -> Result<i64, Error> {
336 let (where_sql, binds) = build_filter_where(eq_filters, like_filters, query, searchable_fields);
337 let sql = format!(
338 "SELECT COUNT(*) FROM {t} WHERE {where_sql}",
339 t = quote_ident(table),
340 );
341 let mut q = sqlx::query_scalar::<_, i64>(&sql);
342 for b in &binds {
343 q = q.bind(b.as_str());
344 }
345 let count = q.fetch_one(db.pool()).await.map_err(Error::from)?;
346 Ok(count)
347}
348
349pub async fn bulk_update(
360 db: &Db,
361 table: &str,
362 ids: &[String],
363 field: &str,
364 value: &str,
365) -> Result<(), Error> {
366 if ids.is_empty() {
367 return Ok(());
368 }
369 let placeholders = vec!["?"; ids.len()].join(", ");
370 let sql = format!(
371 "UPDATE {} SET {} = ? WHERE \"id\" IN ({})",
372 quote_ident(table),
373 quote_ident(field),
374 placeholders,
375 );
376 let mut q = sqlx::query(&sql);
377 q = q.bind(value);
378 for id in ids {
379 q = q.bind(id.as_str());
380 }
381 q.execute(db.pool()).await.map_err(Error::from)?;
382 Ok(())
383}
384
385pub async fn bulk_delete(db: &Db, table: &str, ids: &[String]) -> Result<(), Error> {
388 if ids.is_empty() {
389 return Ok(());
390 }
391 let placeholders = vec!["?"; ids.len()].join(", ");
392 let sql = format!(
393 "DELETE FROM {} WHERE \"id\" IN ({})",
394 quote_ident(table),
395 placeholders,
396 );
397 let mut q = sqlx::query(&sql);
398 for id in ids {
399 q = q.bind(id.as_str());
400 }
401 q.execute(db.pool()).await.map_err(Error::from)?;
402 Ok(())
403}
404
405fn build_filter_where(
413 eq_filters: &HashMap<String, String>,
414 like_filters: &HashMap<String, String>,
415 query: Option<&str>,
416 searchable_fields: &[&str],
417) -> (String, Vec<String>) {
418 let mut clauses: Vec<String> = vec!["1=1".to_string()];
419 let mut binds: Vec<String> = Vec::new();
420
421 let mut eq_keys: Vec<&String> = eq_filters.keys().collect();
422 eq_keys.sort();
423 for k in eq_keys {
424 clauses.push(format!("{} = ?", quote_ident(k)));
425 binds.push(eq_filters.get(k).cloned().unwrap_or_default());
426 }
427
428 let mut like_keys: Vec<&String> = like_filters.keys().collect();
429 like_keys.sort();
430 for k in like_keys {
431 clauses.push(format!("LOWER({}) LIKE ?", quote_ident(k)));
432 let v = like_filters
433 .get(k)
434 .map(|s| s.to_lowercase())
435 .unwrap_or_default();
436 binds.push(format!("%{v}%"));
437 }
438
439 let trimmed_query = query.map(str::trim).filter(|s| !s.is_empty());
440 if let Some(q) = trimmed_query {
441 if !searchable_fields.is_empty() {
442 let or_body = build_search_where(searchable_fields);
443 clauses.push(format!("({or_body})"));
444 let q_param = format!("%{}%", q.to_lowercase());
445 for _ in searchable_fields {
446 binds.push(q_param.clone());
447 }
448 }
449 }
450
451 (clauses.join(" AND "), binds)
452}
453
454fn row_to_map(row: &sqlx::sqlite::SqliteRow) -> HashMap<String, String> {
459 let mut out = HashMap::new();
460 for col in row.columns() {
461 let name = col.name();
462 let value: String = if let Ok(Some(s)) = row.try_get::<Option<String>, _>(name) {
463 s
464 } else if let Ok(Some(n)) = row.try_get::<Option<i64>, _>(name) {
465 n.to_string()
466 } else if let Ok(Some(f)) = row.try_get::<Option<f64>, _>(name) {
467 f.to_string()
468 } else {
469 String::new()
470 };
471 out.insert(name.to_string(), value);
472 }
473 out
474}