ironflow_ops_postgres/
execute.rs1use async_trait::async_trait;
5use ironflow_core::error::OperationError;
6use ironflow_core::operation::{Operation, OperationContext, TypedOperation};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use sqlx::{Executor, PgPool};
10
11use crate::helpers::{bind_json_param, pg_error, to_value};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ExecuteOutput {
16 pub rows_affected: u64,
18}
19
20pub struct Execute {
36 pool: PgPool,
37 sql: String,
38 params: Vec<Value>,
39}
40
41impl Execute {
42 pub fn new(pool: PgPool, sql: impl Into<String>, params: Vec<Value>) -> Self {
44 Self {
45 pool,
46 sql: sql.into(),
47 params,
48 }
49 }
50
51 pub async fn run(&self, _ctx: &OperationContext) -> Result<ExecuteOutput, OperationError> {
57 let mut query = sqlx::query(&self.sql);
58 for p in &self.params {
59 query = bind_json_param(query, p);
60 }
61 let result = query.execute(&self.pool).await.map_err(pg_error)?;
62 Ok(ExecuteOutput {
63 rows_affected: result.rows_affected(),
64 })
65 }
66}
67
68#[async_trait]
69impl Operation for Execute {
70 fn kind(&self) -> &str {
71 "postgres"
72 }
73 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
74 to_value(&self.run(ctx).await?)
75 }
76 fn input(&self) -> Option<Value> {
77 Some(serde_json::json!({ "sql": self.sql, "params": self.params }))
78 }
79}
80
81impl TypedOperation for Execute {
82 type Output = ExecuteOutput;
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct ExecuteBatchOutput {
88 pub rows_affected: Vec<u64>,
90}
91
92pub struct ExecuteBatch {
118 pool: PgPool,
119 statements: Vec<String>,
120}
121
122impl ExecuteBatch {
123 pub fn new(pool: PgPool, statements: Vec<String>) -> Self {
125 Self { pool, statements }
126 }
127
128 pub async fn run(&self, _ctx: &OperationContext) -> Result<ExecuteBatchOutput, OperationError> {
135 let mut results = Vec::with_capacity(self.statements.len());
136 for stmt in &self.statements {
137 let result = self.pool.execute(stmt.as_str()).await.map_err(pg_error)?;
138 results.push(result.rows_affected());
139 }
140 Ok(ExecuteBatchOutput {
141 rows_affected: results,
142 })
143 }
144}
145
146#[async_trait]
147impl Operation for ExecuteBatch {
148 fn kind(&self) -> &str {
149 "postgres"
150 }
151 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
152 to_value(&self.run(ctx).await?)
153 }
154 fn input(&self) -> Option<Value> {
155 Some(serde_json::json!({ "statements": self.statements }))
156 }
157}
158
159impl TypedOperation for ExecuteBatch {
160 type Output = ExecuteBatchOutput;
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct TransactionOutput {
166 pub rows_affected: Vec<u64>,
168}
169
170pub struct Transaction {
195 pool: PgPool,
196 statements: Vec<String>,
197}
198
199impl Transaction {
200 pub fn new(pool: PgPool, statements: Vec<String>) -> Self {
202 Self { pool, statements }
203 }
204
205 pub async fn run(&self, _ctx: &OperationContext) -> Result<TransactionOutput, OperationError> {
212 let mut tx = self.pool.begin().await.map_err(pg_error)?;
213 let mut results = Vec::with_capacity(self.statements.len());
214 for stmt in &self.statements {
215 let result = tx.execute(stmt.as_str()).await.map_err(pg_error)?;
216 results.push(result.rows_affected());
217 }
218 tx.commit().await.map_err(pg_error)?;
219 Ok(TransactionOutput {
220 rows_affected: results,
221 })
222 }
223}
224
225#[async_trait]
226impl Operation for Transaction {
227 fn kind(&self) -> &str {
228 "postgres"
229 }
230 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
231 to_value(&self.run(ctx).await?)
232 }
233 fn input(&self) -> Option<Value> {
234 Some(serde_json::json!({ "statements": self.statements }))
235 }
236}
237
238impl TypedOperation for Transaction {
239 type Output = TransactionOutput;
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[tokio::test]
247 async fn execute_kind() {
248 let pool = PgPool::connect_lazy("postgres://localhost/test").unwrap();
249 let op = Execute::new(pool, "INSERT INTO t VALUES (1)", vec![]);
250 assert_eq!(op.kind(), "postgres");
251 }
252
253 #[tokio::test]
254 async fn execute_batch_kind() {
255 let pool = PgPool::connect_lazy("postgres://localhost/test").unwrap();
256 let op = ExecuteBatch::new(pool, vec!["SELECT 1".to_string()]);
257 assert_eq!(op.kind(), "postgres");
258 }
259
260 #[tokio::test]
261 async fn transaction_kind() {
262 let pool = PgPool::connect_lazy("postgres://localhost/test").unwrap();
263 let op = Transaction::new(pool, vec!["SELECT 1".to_string()]);
264 assert_eq!(op.kind(), "postgres");
265 }
266
267 #[tokio::test]
268 async fn execute_input_no_secrets() {
269 let pool = PgPool::connect_lazy("postgres://localhost/test").unwrap();
270 let op = Execute::new(pool, "INSERT INTO t VALUES ($1)", vec![]);
271 let input = op.input().unwrap();
272 let text = input.to_string();
273 assert!(!text.contains("postgres://"), "leaked URL: {text}");
274 }
275
276 #[tokio::test]
277 async fn execute_batch_input_no_secrets() {
278 let pool = PgPool::connect_lazy("postgres://localhost/test").unwrap();
279 let op = ExecuteBatch::new(pool, vec!["SELECT 1".to_string()]);
280 let input = op.input().unwrap();
281 let text = input.to_string();
282 assert!(!text.contains("postgres://"), "leaked URL: {text}");
283 }
284
285 #[tokio::test]
286 async fn transaction_input_no_secrets() {
287 let pool = PgPool::connect_lazy("postgres://localhost/test").unwrap();
288 let op = Transaction::new(pool, vec!["SELECT 1".to_string()]);
289 let input = op.input().unwrap();
290 let text = input.to_string();
291 assert!(!text.contains("postgres://"), "leaked URL: {text}");
292 }
293}