ironflow_ops_postgres/schema/
tables.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::{PgPool, Row};
9
10use crate::helpers::{pg_error, to_value};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ListTablesOutput {
15 pub tables: Vec<String>,
17}
18
19pub struct ListTables {
33 pool: PgPool,
34 schema: String,
35}
36
37impl ListTables {
38 pub fn new(pool: PgPool, schema: impl Into<String>) -> Self {
40 Self {
41 pool,
42 schema: schema.into(),
43 }
44 }
45
46 pub async fn run(&self, _ctx: &OperationContext) -> Result<ListTablesOutput, OperationError> {
52 let rows = sqlx::query(
53 "SELECT table_name FROM information_schema.tables \
54 WHERE table_schema = $1 AND table_type = 'BASE TABLE' \
55 ORDER BY table_name",
56 )
57 .bind(&self.schema)
58 .fetch_all(&self.pool)
59 .await
60 .map_err(pg_error)?;
61 let tables = rows
62 .iter()
63 .map(|r| r.try_get::<String, _>("table_name").map_err(pg_error))
64 .collect::<Result<Vec<_>, _>>()?;
65 Ok(ListTablesOutput { tables })
66 }
67}
68
69#[async_trait]
70impl Operation for ListTables {
71 fn kind(&self) -> &str {
72 "postgres"
73 }
74 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
75 to_value(&self.run(ctx).await?)
76 }
77 fn input(&self) -> Option<Value> {
78 Some(serde_json::json!({ "schema": self.schema }))
79 }
80}
81
82impl TypedOperation for ListTables {
83 type Output = ListTablesOutput;
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct TableExistsOutput {
89 pub exists: bool,
91}
92
93pub struct TableExists {
107 pool: PgPool,
108 schema: String,
109 table: String,
110}
111
112impl TableExists {
113 pub fn new(pool: PgPool, schema: impl Into<String>, table: impl Into<String>) -> Self {
115 Self {
116 pool,
117 schema: schema.into(),
118 table: table.into(),
119 }
120 }
121
122 pub async fn run(&self, _ctx: &OperationContext) -> Result<TableExistsOutput, OperationError> {
128 let row = sqlx::query(
129 "SELECT EXISTS( \
130 SELECT 1 FROM information_schema.tables \
131 WHERE table_schema = $1 AND table_name = $2 \
132 ) AS exists",
133 )
134 .bind(&self.schema)
135 .bind(&self.table)
136 .fetch_one(&self.pool)
137 .await
138 .map_err(pg_error)?;
139 let exists: bool = row.try_get("exists").map_err(pg_error)?;
140 Ok(TableExistsOutput { exists })
141 }
142}
143
144#[async_trait]
145impl Operation for TableExists {
146 fn kind(&self) -> &str {
147 "postgres"
148 }
149 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
150 to_value(&self.run(ctx).await?)
151 }
152 fn input(&self) -> Option<Value> {
153 Some(serde_json::json!({ "schema": self.schema, "table": self.table }))
154 }
155}
156
157impl TypedOperation for TableExists {
158 type Output = TableExistsOutput;
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[tokio::test]
166 async fn list_tables_kind() {
167 let pool = PgPool::connect_lazy("postgres://localhost/test").unwrap();
168 let op = ListTables::new(pool, "public");
169 assert_eq!(op.kind(), "postgres");
170 }
171
172 #[tokio::test]
173 async fn table_exists_kind() {
174 let pool = PgPool::connect_lazy("postgres://localhost/test").unwrap();
175 let op = TableExists::new(pool, "public", "users");
176 assert_eq!(op.kind(), "postgres");
177 }
178
179 #[tokio::test]
180 async fn list_tables_input_no_secrets() {
181 let pool = PgPool::connect_lazy("postgres://localhost/test").unwrap();
182 let op = ListTables::new(pool, "public");
183 let input = op.input().unwrap();
184 let text = input.to_string();
185 assert!(!text.contains("postgres://"), "leaked URL: {text}");
186 }
187}