use crate::sql::codec::{bind_any, json_to_any_value, row_to_json};
use crate::sql::introspect::ColumnInfo;
use axum::http::StatusCode;
use larust_core::AppError;
use serde_json::Value as Json;
fn internal<E: std::error::Error + Send + Sync + 'static>(error: E) -> AppError {
AppError::Internal(Box::new(error))
}
fn column_kind<'a>(columns: &'a [ColumnInfo], name: &str) -> sqlx::any::AnyTypeInfoKind {
columns
.iter()
.find(|c| c.name == name)
.map(|c: &'a ColumnInfo| c.kind)
.unwrap_or(sqlx::any::AnyTypeInfoKind::Text)
}
fn require_known_pk_columns(columns: &[ColumnInfo], pk: &[(String, Json)]) -> Result<(), AppError> {
for (name, _) in pk {
if !columns.iter().any(|c| &c.name == name) {
return Err(AppError::Http {
status: StatusCode::BAD_REQUEST,
message: format!("unknown primary key column: {name}"),
});
}
}
Ok(())
}
pub async fn insert_row(
table: &str,
columns: &[ColumnInfo],
values: &[(String, Json)],
) -> Result<(), AppError> {
let backend = larust_orm::backend();
let present: Vec<&(String, Json)> = values
.iter()
.filter(|(name, _)| columns.iter().any(|c| &c.name == name))
.collect();
if present.is_empty() {
return Ok(());
}
let column_list = present
.iter()
.map(|(name, _)| format!("\"{name}\""))
.collect::<Vec<_>>()
.join(", ");
let placeholders = (1..=present.len())
.map(|n| larust_orm::placeholder(backend, n))
.collect::<Vec<_>>()
.join(", ");
let sql = format!("INSERT INTO \"{table}\" ({column_list}) VALUES ({placeholders})");
let pool = larust_orm::pool()?;
let mut query = sqlx::query(&sql);
for (name, value) in &present {
query = bind_any(query, json_to_any_value(value, column_kind(columns, name)));
}
query.execute(pool).await.map_err(internal)?;
Ok(())
}
pub async fn update_row(
table: &str,
columns: &[ColumnInfo],
pk: &[(String, Json)],
values: &[(String, Json)],
) -> Result<(), AppError> {
require_known_pk_columns(columns, pk)?;
let backend = larust_orm::backend();
let settable: Vec<&(String, Json)> = values
.iter()
.filter(|(name, _)| {
columns.iter().any(|c| &c.name == name)
&& !pk.iter().any(|(pk_name, _)| pk_name == name)
})
.collect();
if settable.is_empty() {
return Ok(());
}
let mut n = 0usize;
let set_clause = settable
.iter()
.map(|(name, _)| {
n += 1;
format!("\"{name}\" = {}", larust_orm::placeholder(backend, n))
})
.collect::<Vec<_>>()
.join(", ");
let where_clause = pk
.iter()
.map(|(name, _)| {
n += 1;
format!("\"{name}\" = {}", larust_orm::placeholder(backend, n))
})
.collect::<Vec<_>>()
.join(" AND ");
let sql = format!("UPDATE \"{table}\" SET {set_clause} WHERE {where_clause}");
let pool = larust_orm::pool()?;
let mut query = sqlx::query(&sql);
for (name, value) in &settable {
query = bind_any(query, json_to_any_value(value, column_kind(columns, name)));
}
for (name, value) in pk {
query = bind_any(query, json_to_any_value(value, column_kind(columns, name)));
}
query.execute(pool).await.map_err(internal)?;
Ok(())
}
pub async fn delete_row(
table: &str,
columns: &[ColumnInfo],
pk: &[(String, Json)],
) -> Result<(), AppError> {
require_known_pk_columns(columns, pk)?;
let backend = larust_orm::backend();
let mut n = 0usize;
let where_clause = pk
.iter()
.map(|(name, _)| {
n += 1;
format!("\"{name}\" = {}", larust_orm::placeholder(backend, n))
})
.collect::<Vec<_>>()
.join(" AND ");
let sql = format!("DELETE FROM \"{table}\" WHERE {where_clause}");
let pool = larust_orm::pool()?;
let mut query = sqlx::query(&sql);
for (name, value) in pk {
query = bind_any(query, json_to_any_value(value, column_kind(columns, name)));
}
query.execute(pool).await.map_err(internal)?;
Ok(())
}
pub async fn fetch_row(
table: &str,
columns: &[ColumnInfo],
pk: &[(String, Json)],
) -> Result<Option<Json>, AppError> {
require_known_pk_columns(columns, pk)?;
let backend = larust_orm::backend();
let mut n = 0usize;
let where_clause = pk
.iter()
.map(|(name, _)| {
n += 1;
format!("\"{name}\" = {}", larust_orm::placeholder(backend, n))
})
.collect::<Vec<_>>()
.join(" AND ");
let sql = format!("SELECT * FROM \"{table}\" WHERE {where_clause}");
let pool = larust_orm::pool()?;
let mut query = sqlx::query(&sql);
for (name, value) in pk {
query = bind_any(query, json_to_any_value(value, column_kind(columns, name)));
}
let row = query.fetch_optional(pool).await.map_err(internal)?;
Ok(row.as_ref().map(row_to_json))
}
pub async fn run_raw(sql: &str) -> Result<Vec<Json>, AppError> {
let pool = larust_orm::pool()?;
let rows = sqlx::query(sql).fetch_all(pool).await.map_err(internal)?;
Ok(rows.iter().map(row_to_json).collect())
}
pub async fn run_script(sql: &str) -> Result<(), AppError> {
let pool = larust_orm::pool()?;
sqlx::raw_sql(sql).execute(pool).await.map_err(internal)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn id_column() -> ColumnInfo {
ColumnInfo {
name: "id".to_string(),
not_null: true,
kind: sqlx::any::AnyTypeInfoKind::BigInt,
}
}
#[test]
fn require_known_pk_columns_accepts_a_real_column() {
let columns = [id_column()];
let pk = [("id".to_string(), Json::from(1))];
assert!(require_known_pk_columns(&columns, &pk).is_ok());
}
#[test]
fn require_known_pk_columns_rejects_an_unknown_column_name() {
let columns = [id_column()];
let pk = [(r#"id" OR "1"="1"#.to_string(), Json::from(1))];
let error = require_known_pk_columns(&columns, &pk).unwrap_err();
assert!(matches!(
error,
AppError::Http {
status: StatusCode::BAD_REQUEST,
..
}
));
}
}