use serde_json::Value;
use sqlx_core::row::Row;
use sqlx_core::sql_str::AssertSqlSafe;
use sqlx_postgres::{PgPool, PgRow};
use crate::column::{ColumnInfo, ColumnType};
use crate::sql_generator::{GeneratedColumn, SqlGenerator};
use crate::view_definition::ViewDefinition;
use crate::{Error, Result};
pub struct ViewRunner {
pool: PgPool,
generator: SqlGenerator,
}
impl ViewRunner {
pub fn new(pool: PgPool) -> Self {
Self {
pool,
generator: SqlGenerator::new(),
}
}
pub fn with_generator(pool: PgPool, generator: SqlGenerator) -> Self {
Self { pool, generator }
}
pub async fn run(&self, view: &ViewDefinition) -> Result<ViewResult> {
let generated = self.generator.generate(view)?;
tracing::debug!(sql = %generated.sql, "Executing view");
let rows = sqlx_core::query::query(AssertSqlSafe(generated.sql.clone()))
.fetch_all(&self.pool)
.await
.map_err(Error::Sql)?;
let columns: Vec<ColumnInfo> = generated
.columns
.iter()
.map(|c| ColumnInfo::new(c.alias.clone(), c.col_type))
.collect();
let data: Vec<Vec<Value>> = rows
.iter()
.map(|row| extract_row_values(row, &generated.columns))
.collect();
Ok(ViewResult {
columns,
data,
row_count: rows.len(),
})
}
pub fn explain(&self, view: &ViewDefinition) -> Result<String> {
let generated = self.generator.generate(view)?;
Ok(generated.sql)
}
pub fn pool(&self) -> &PgPool {
&self.pool
}
}
#[async_trait::async_trait]
pub trait SqlExecutor {
async fn fetch(&self, sql: &str, columns: &[GeneratedColumn]) -> Result<Vec<Vec<Value>>>;
}
pub async fn run_with<E: SqlExecutor + ?Sized>(
executor: &E,
generator: &SqlGenerator,
view: &ViewDefinition,
) -> Result<ViewResult> {
let generated = generator.generate(view)?;
let data = executor.fetch(&generated.sql, &generated.columns).await?;
let columns: Vec<ColumnInfo> = generated
.columns
.iter()
.map(|c| ColumnInfo::new(c.alias.clone(), c.col_type))
.collect();
let row_count = data.len();
Ok(ViewResult {
columns,
data,
row_count,
})
}
fn extract_row_values(row: &PgRow, columns: &[GeneratedColumn]) -> Vec<Value> {
columns
.iter()
.enumerate()
.map(|(i, col)| extract_column_value(row, i, &col.col_type))
.collect()
}
fn extract_column_value(row: &PgRow, index: usize, col_type: &ColumnType) -> Value {
match col_type {
ColumnType::Integer | ColumnType::Integer64 => row
.try_get::<Option<i64>, _>(index)
.ok()
.flatten()
.map(|v| Value::Number(v.into()))
.unwrap_or(Value::Null),
ColumnType::Decimal => row
.try_get::<Option<f64>, _>(index)
.ok()
.flatten()
.and_then(|v| serde_json::Number::from_f64(v).map(Value::Number))
.unwrap_or(Value::Null),
ColumnType::Boolean => row
.try_get::<Option<bool>, _>(index)
.ok()
.flatten()
.map(Value::Bool)
.unwrap_or(Value::Null),
ColumnType::Json => row
.try_get::<Option<Value>, _>(index)
.ok()
.flatten()
.unwrap_or(Value::Null),
ColumnType::String
| ColumnType::Date
| ColumnType::DateTime
| ColumnType::Instant
| ColumnType::Time
| ColumnType::Base64Binary => row
.try_get::<Option<String>, _>(index)
.ok()
.flatten()
.map(Value::String)
.unwrap_or(Value::Null),
}
}
#[derive(Debug, Clone)]
pub struct ViewResult {
pub columns: Vec<ColumnInfo>,
pub data: Vec<Vec<Value>>,
pub row_count: usize,
}
impl ViewResult {
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn column_count(&self) -> usize {
self.columns.len()
}
pub fn to_json_array(&self) -> Vec<Value> {
self.data
.iter()
.map(|row| {
let mut obj = serde_json::Map::new();
for (i, col) in self.columns.iter().enumerate() {
if let Some(value) = row.get(i) {
obj.insert(col.name.clone(), value.clone());
}
}
Value::Object(obj)
})
.collect()
}
pub fn row_as_object(&self, index: usize) -> Option<Value> {
self.data.get(index).map(|row| {
let mut obj = serde_json::Map::new();
for (i, col) in self.columns.iter().enumerate() {
if let Some(value) = row.get(i) {
obj.insert(col.name.clone(), value.clone());
}
}
Value::Object(obj)
})
}
pub fn column_values(&self, name: &str) -> Option<Vec<&Value>> {
let col_index = self.columns.iter().position(|c| c.name == name)?;
Some(
self.data
.iter()
.filter_map(|row| row.get(col_index))
.collect(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_view_result_to_json_array() {
let result = ViewResult {
columns: vec![
ColumnInfo::new("id", ColumnType::String),
ColumnInfo::new("name", ColumnType::String),
],
data: vec![
vec![json!("1"), json!("Alice")],
vec![json!("2"), json!("Bob")],
],
row_count: 2,
};
let json_array = result.to_json_array();
assert_eq!(json_array.len(), 2);
assert_eq!(json_array[0]["id"], "1");
assert_eq!(json_array[0]["name"], "Alice");
assert_eq!(json_array[1]["id"], "2");
assert_eq!(json_array[1]["name"], "Bob");
}
#[test]
fn test_view_result_row_as_object() {
let result = ViewResult {
columns: vec![
ColumnInfo::new("id", ColumnType::String),
ColumnInfo::new("active", ColumnType::Boolean),
],
data: vec![vec![json!("123"), json!(true)]],
row_count: 1,
};
let row = result.row_as_object(0).unwrap();
assert_eq!(row["id"], "123");
assert_eq!(row["active"], true);
assert!(result.row_as_object(1).is_none());
}
#[test]
fn test_view_result_column_values() {
let result = ViewResult {
columns: vec![
ColumnInfo::new("id", ColumnType::String),
ColumnInfo::new("value", ColumnType::Integer),
],
data: vec![
vec![json!("1"), json!(10)],
vec![json!("2"), json!(20)],
vec![json!("3"), json!(30)],
],
row_count: 3,
};
let ids = result.column_values("id").unwrap();
assert_eq!(ids.len(), 3);
assert_eq!(*ids[0], json!("1"));
let values = result.column_values("value").unwrap();
assert_eq!(values.len(), 3);
assert_eq!(*values[0], json!(10));
assert!(result.column_values("nonexistent").is_none());
}
#[test]
fn test_view_result_is_empty() {
let empty_result = ViewResult {
columns: vec![ColumnInfo::new("id", ColumnType::String)],
data: vec![],
row_count: 0,
};
assert!(empty_result.is_empty());
let non_empty = ViewResult {
columns: vec![ColumnInfo::new("id", ColumnType::String)],
data: vec![vec![json!("1")]],
row_count: 1,
};
assert!(!non_empty.is_empty());
}
}