ironflow_ops_postgres/
maintenance.rs1use async_trait::async_trait;
4use ironflow_core::error::OperationError;
5use ironflow_core::operation::{Operation, OperationContext, TypedOperation};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use sqlx::{Executor, PgPool};
9
10use crate::helpers::{pg_error, quote_identifier, to_value};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct MaintenanceOutput {
15 pub operation: String,
17 pub target: String,
19}
20
21pub struct Vacuum {
35 pool: PgPool,
36 schema: String,
37 table: String,
38}
39
40impl Vacuum {
41 pub fn new(pool: PgPool, schema: impl Into<String>, table: impl Into<String>) -> Self {
43 Self {
44 pool,
45 schema: schema.into(),
46 table: table.into(),
47 }
48 }
49
50 pub async fn run(&self, _ctx: &OperationContext) -> Result<MaintenanceOutput, OperationError> {
56 let schema = quote_identifier(&self.schema)?;
57 let table = quote_identifier(&self.table)?;
58 let sql = format!("VACUUM {schema}.{table}");
59 self.pool.execute(sql.as_str()).await.map_err(pg_error)?;
60 Ok(MaintenanceOutput {
61 operation: "VACUUM".to_string(),
62 target: format!("{}.{}", self.schema, self.table),
63 })
64 }
65}
66
67#[async_trait]
68impl Operation for Vacuum {
69 fn kind(&self) -> &str {
70 "postgres"
71 }
72 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
73 to_value(&self.run(ctx).await?)
74 }
75 fn input(&self) -> Option<Value> {
76 Some(serde_json::json!({ "schema": self.schema, "table": self.table }))
77 }
78}
79
80impl TypedOperation for Vacuum {
81 type Output = MaintenanceOutput;
82}
83
84pub struct Analyze {
98 pool: PgPool,
99 schema: String,
100 table: String,
101}
102
103impl Analyze {
104 pub fn new(pool: PgPool, schema: impl Into<String>, table: impl Into<String>) -> Self {
106 Self {
107 pool,
108 schema: schema.into(),
109 table: table.into(),
110 }
111 }
112
113 pub async fn run(&self, _ctx: &OperationContext) -> Result<MaintenanceOutput, OperationError> {
119 let schema = quote_identifier(&self.schema)?;
120 let table = quote_identifier(&self.table)?;
121 let sql = format!("ANALYZE {schema}.{table}");
122 self.pool.execute(sql.as_str()).await.map_err(pg_error)?;
123 Ok(MaintenanceOutput {
124 operation: "ANALYZE".to_string(),
125 target: format!("{}.{}", self.schema, self.table),
126 })
127 }
128}
129
130#[async_trait]
131impl Operation for Analyze {
132 fn kind(&self) -> &str {
133 "postgres"
134 }
135 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
136 to_value(&self.run(ctx).await?)
137 }
138 fn input(&self) -> Option<Value> {
139 Some(serde_json::json!({ "schema": self.schema, "table": self.table }))
140 }
141}
142
143impl TypedOperation for Analyze {
144 type Output = MaintenanceOutput;
145}
146
147pub struct Reindex {
161 pool: PgPool,
162 schema: String,
163 table: String,
164}
165
166impl Reindex {
167 pub fn new(pool: PgPool, schema: impl Into<String>, table: impl Into<String>) -> Self {
169 Self {
170 pool,
171 schema: schema.into(),
172 table: table.into(),
173 }
174 }
175
176 pub async fn run(&self, _ctx: &OperationContext) -> Result<MaintenanceOutput, OperationError> {
182 let schema = quote_identifier(&self.schema)?;
183 let table = quote_identifier(&self.table)?;
184 let sql = format!("REINDEX TABLE {schema}.{table}");
185 self.pool.execute(sql.as_str()).await.map_err(pg_error)?;
186 Ok(MaintenanceOutput {
187 operation: "REINDEX".to_string(),
188 target: format!("{}.{}", self.schema, self.table),
189 })
190 }
191}
192
193#[async_trait]
194impl Operation for Reindex {
195 fn kind(&self) -> &str {
196 "postgres"
197 }
198 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
199 to_value(&self.run(ctx).await?)
200 }
201 fn input(&self) -> Option<Value> {
202 Some(serde_json::json!({ "schema": self.schema, "table": self.table }))
203 }
204}
205
206impl TypedOperation for Reindex {
207 type Output = MaintenanceOutput;
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[tokio::test]
215 async fn vacuum_kind() {
216 let pool = PgPool::connect_lazy("postgres://localhost/test").unwrap();
217 let op = Vacuum::new(pool, "public", "users");
218 assert_eq!(op.kind(), "postgres");
219 }
220
221 #[tokio::test]
222 async fn analyze_kind() {
223 let pool = PgPool::connect_lazy("postgres://localhost/test").unwrap();
224 let op = Analyze::new(pool, "public", "users");
225 assert_eq!(op.kind(), "postgres");
226 }
227
228 #[tokio::test]
229 async fn reindex_kind() {
230 let pool = PgPool::connect_lazy("postgres://localhost/test").unwrap();
231 let op = Reindex::new(pool, "public", "users");
232 assert_eq!(op.kind(), "postgres");
233 }
234
235 #[tokio::test]
236 async fn vacuum_input_no_secrets() {
237 let pool = PgPool::connect_lazy("postgres://localhost/test").unwrap();
238 let op = Vacuum::new(pool, "public", "users");
239 let input = op.input().unwrap();
240 let text = input.to_string();
241 assert!(!text.contains("postgres://"), "leaked URL: {text}");
242 }
243}