Skip to main content

octofhir_sof/
runner.rs

1//! View execution against PostgreSQL.
2//!
3//! This module provides the ViewRunner which executes generated SQL
4//! against a PostgreSQL database and returns structured results.
5
6use serde_json::Value;
7use sqlx_core::row::Row;
8use sqlx_core::sql_str::AssertSqlSafe;
9use sqlx_postgres::{PgPool, PgRow};
10
11use crate::column::{ColumnInfo, ColumnType};
12use crate::sql_generator::{GeneratedColumn, SqlGenerator};
13use crate::view_definition::ViewDefinition;
14use crate::{Error, Result};
15
16/// Executes ViewDefinitions against a PostgreSQL database.
17pub struct ViewRunner {
18    pool: PgPool,
19    generator: SqlGenerator,
20}
21
22impl ViewRunner {
23    /// Create a new ViewRunner with the given connection pool.
24    pub fn new(pool: PgPool) -> Self {
25        Self {
26            pool,
27            generator: SqlGenerator::new(),
28        }
29    }
30
31    /// Create a new ViewRunner with a custom SQL generator.
32    pub fn with_generator(pool: PgPool, generator: SqlGenerator) -> Self {
33        Self { pool, generator }
34    }
35
36    /// Execute a ViewDefinition and return the results.
37    ///
38    /// # Errors
39    ///
40    /// Returns an error if SQL generation fails or the query fails to execute.
41    pub async fn run(&self, view: &ViewDefinition) -> Result<ViewResult> {
42        let generated = self.generator.generate(view)?;
43
44        tracing::debug!(sql = %generated.sql, "Executing view");
45
46        let rows = sqlx_core::query::query(AssertSqlSafe(generated.sql.clone()))
47            .fetch_all(&self.pool)
48            .await
49            .map_err(Error::Sql)?;
50
51        let columns: Vec<ColumnInfo> = generated
52            .columns
53            .iter()
54            .map(|c| ColumnInfo::new(c.alias.clone(), c.col_type))
55            .collect();
56
57        let data: Vec<Vec<Value>> = rows
58            .iter()
59            .map(|row| extract_row_values(row, &generated.columns))
60            .collect();
61
62        Ok(ViewResult {
63            columns,
64            data,
65            row_count: rows.len(),
66        })
67    }
68
69    /// Execute a ViewDefinition and return only the generated SQL.
70    ///
71    /// This is useful for debugging or explaining the query without executing it.
72    pub fn explain(&self, view: &ViewDefinition) -> Result<String> {
73        let generated = self.generator.generate(view)?;
74        Ok(generated.sql)
75    }
76
77    /// Get a reference to the underlying connection pool.
78    pub fn pool(&self) -> &PgPool {
79        &self.pool
80    }
81}
82
83/// Fetches rows for SoF-generated SQL through an external database layer.
84///
85/// Implement this to run the SQL produced by [`SqlGenerator`] with your own
86/// connection pool / driver instead of the bundled sqlx [`ViewRunner`] — for
87/// example to reuse a host application's instrumented Postgres pool. Each
88/// returned inner `Vec` is one result row, with values in the same order as the
89/// supplied `columns`; implementations should coerce each value according to
90/// the column's [`ColumnType`].
91#[async_trait::async_trait]
92pub trait SqlExecutor {
93    /// Execute `sql` and return its rows as JSON values in `columns` order.
94    async fn fetch(&self, sql: &str, columns: &[GeneratedColumn]) -> Result<Vec<Vec<Value>>>;
95}
96
97/// Execute a ViewDefinition by generating SQL and delegating row fetching to an
98/// external [`SqlExecutor`]. Produces the same [`ViewResult`] as
99/// [`ViewRunner::run`], so callers can swap the database layer without changing
100/// downstream handling.
101///
102/// # Errors
103///
104/// Returns an error if SQL generation fails or the executor fails to fetch rows.
105pub async fn run_with<E: SqlExecutor + ?Sized>(
106    executor: &E,
107    generator: &SqlGenerator,
108    view: &ViewDefinition,
109) -> Result<ViewResult> {
110    let generated = generator.generate(view)?;
111    let data = executor.fetch(&generated.sql, &generated.columns).await?;
112    let columns: Vec<ColumnInfo> = generated
113        .columns
114        .iter()
115        .map(|c| ColumnInfo::new(c.alias.clone(), c.col_type))
116        .collect();
117    let row_count = data.len();
118    Ok(ViewResult {
119        columns,
120        data,
121        row_count,
122    })
123}
124
125/// Extract values from a row based on column definitions.
126fn extract_row_values(row: &PgRow, columns: &[GeneratedColumn]) -> Vec<Value> {
127    columns
128        .iter()
129        .enumerate()
130        .map(|(i, col)| extract_column_value(row, i, &col.col_type))
131        .collect()
132}
133
134/// Extract a single column value from a row.
135fn extract_column_value(row: &PgRow, index: usize, col_type: &ColumnType) -> Value {
136    match col_type {
137        ColumnType::Integer | ColumnType::Integer64 => row
138            .try_get::<Option<i64>, _>(index)
139            .ok()
140            .flatten()
141            .map(|v| Value::Number(v.into()))
142            .unwrap_or(Value::Null),
143
144        ColumnType::Decimal => row
145            .try_get::<Option<f64>, _>(index)
146            .ok()
147            .flatten()
148            .and_then(|v| serde_json::Number::from_f64(v).map(Value::Number))
149            .unwrap_or(Value::Null),
150
151        ColumnType::Boolean => row
152            .try_get::<Option<bool>, _>(index)
153            .ok()
154            .flatten()
155            .map(Value::Bool)
156            .unwrap_or(Value::Null),
157
158        ColumnType::Json => row
159            .try_get::<Option<Value>, _>(index)
160            .ok()
161            .flatten()
162            .unwrap_or(Value::Null),
163
164        // String-like types
165        ColumnType::String
166        | ColumnType::Date
167        | ColumnType::DateTime
168        | ColumnType::Instant
169        | ColumnType::Time
170        | ColumnType::Base64Binary => row
171            .try_get::<Option<String>, _>(index)
172            .ok()
173            .flatten()
174            .map(Value::String)
175            .unwrap_or(Value::Null),
176    }
177}
178
179/// Results from executing a ViewDefinition.
180#[derive(Debug, Clone)]
181pub struct ViewResult {
182    /// Column metadata for the result set.
183    pub columns: Vec<ColumnInfo>,
184
185    /// Row data as JSON values.
186    pub data: Vec<Vec<Value>>,
187
188    /// Total number of rows returned.
189    pub row_count: usize,
190}
191
192impl ViewResult {
193    /// Check if the result set is empty.
194    pub fn is_empty(&self) -> bool {
195        self.data.is_empty()
196    }
197
198    /// Get the number of columns.
199    pub fn column_count(&self) -> usize {
200        self.columns.len()
201    }
202
203    /// Convert the result to a JSON array of objects.
204    pub fn to_json_array(&self) -> Vec<Value> {
205        self.data
206            .iter()
207            .map(|row| {
208                let mut obj = serde_json::Map::new();
209                for (i, col) in self.columns.iter().enumerate() {
210                    if let Some(value) = row.get(i) {
211                        obj.insert(col.name.clone(), value.clone());
212                    }
213                }
214                Value::Object(obj)
215            })
216            .collect()
217    }
218
219    /// Get a single row by index as a JSON object.
220    pub fn row_as_object(&self, index: usize) -> Option<Value> {
221        self.data.get(index).map(|row| {
222            let mut obj = serde_json::Map::new();
223            for (i, col) in self.columns.iter().enumerate() {
224                if let Some(value) = row.get(i) {
225                    obj.insert(col.name.clone(), value.clone());
226                }
227            }
228            Value::Object(obj)
229        })
230    }
231
232    /// Get column values by name.
233    pub fn column_values(&self, name: &str) -> Option<Vec<&Value>> {
234        let col_index = self.columns.iter().position(|c| c.name == name)?;
235        Some(
236            self.data
237                .iter()
238                .filter_map(|row| row.get(col_index))
239                .collect(),
240        )
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use serde_json::json;
248
249    #[test]
250    fn test_view_result_to_json_array() {
251        let result = ViewResult {
252            columns: vec![
253                ColumnInfo::new("id", ColumnType::String),
254                ColumnInfo::new("name", ColumnType::String),
255            ],
256            data: vec![
257                vec![json!("1"), json!("Alice")],
258                vec![json!("2"), json!("Bob")],
259            ],
260            row_count: 2,
261        };
262
263        let json_array = result.to_json_array();
264        assert_eq!(json_array.len(), 2);
265        assert_eq!(json_array[0]["id"], "1");
266        assert_eq!(json_array[0]["name"], "Alice");
267        assert_eq!(json_array[1]["id"], "2");
268        assert_eq!(json_array[1]["name"], "Bob");
269    }
270
271    #[test]
272    fn test_view_result_row_as_object() {
273        let result = ViewResult {
274            columns: vec![
275                ColumnInfo::new("id", ColumnType::String),
276                ColumnInfo::new("active", ColumnType::Boolean),
277            ],
278            data: vec![vec![json!("123"), json!(true)]],
279            row_count: 1,
280        };
281
282        let row = result.row_as_object(0).unwrap();
283        assert_eq!(row["id"], "123");
284        assert_eq!(row["active"], true);
285
286        assert!(result.row_as_object(1).is_none());
287    }
288
289    #[test]
290    fn test_view_result_column_values() {
291        let result = ViewResult {
292            columns: vec![
293                ColumnInfo::new("id", ColumnType::String),
294                ColumnInfo::new("value", ColumnType::Integer),
295            ],
296            data: vec![
297                vec![json!("1"), json!(10)],
298                vec![json!("2"), json!(20)],
299                vec![json!("3"), json!(30)],
300            ],
301            row_count: 3,
302        };
303
304        let ids = result.column_values("id").unwrap();
305        assert_eq!(ids.len(), 3);
306        assert_eq!(*ids[0], json!("1"));
307
308        let values = result.column_values("value").unwrap();
309        assert_eq!(values.len(), 3);
310        assert_eq!(*values[0], json!(10));
311
312        assert!(result.column_values("nonexistent").is_none());
313    }
314
315    #[test]
316    fn test_view_result_is_empty() {
317        let empty_result = ViewResult {
318            columns: vec![ColumnInfo::new("id", ColumnType::String)],
319            data: vec![],
320            row_count: 0,
321        };
322        assert!(empty_result.is_empty());
323
324        let non_empty = ViewResult {
325            columns: vec![ColumnInfo::new("id", ColumnType::String)],
326            data: vec![vec![json!("1")]],
327            row_count: 1,
328        };
329        assert!(!non_empty.is_empty());
330    }
331}