Skip to main content

ironflow_ops_postgres/schema/
constraints.rs

1//! Constraint introspection operation.
2
3use 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/// A constraint description.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ConstraintInfo {
15    /// Constraint name.
16    pub name: String,
17    /// Constraint type (`PRIMARY KEY`, `FOREIGN KEY`, `UNIQUE`, `CHECK`).
18    pub constraint_type: String,
19    /// Column names involved, if applicable.
20    pub columns: Vec<String>,
21}
22
23/// Output of [`ListConstraints`].
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ListConstraintsOutput {
26    /// Constraints on the table.
27    pub constraints: Vec<ConstraintInfo>,
28}
29
30/// List the constraints on a table.
31///
32/// # Examples
33///
34/// ```no_run
35/// use ironflow_ops_postgres::schema::constraints::ListConstraints;
36/// use ironflow_core::operation::Operation;
37///
38/// # fn example(pool: sqlx::PgPool) {
39/// let op = ListConstraints::new(pool, "public", "users");
40/// assert_eq!(op.kind(), "postgres");
41/// # }
42/// ```
43pub struct ListConstraints {
44    pool: PgPool,
45    schema: String,
46    table: String,
47}
48
49impl ListConstraints {
50    /// Create a new list-constraints operation.
51    pub fn new(pool: PgPool, schema: impl Into<String>, table: impl Into<String>) -> Self {
52        Self {
53            pool,
54            schema: schema.into(),
55            table: table.into(),
56        }
57    }
58
59    /// Execute and return a typed result.
60    ///
61    /// # Errors
62    ///
63    /// Returns [`OperationError::External`] on connection errors.
64    pub async fn run(
65        &self,
66        _ctx: &OperationContext,
67    ) -> Result<ListConstraintsOutput, OperationError> {
68        let rows = sqlx::query(
69            "SELECT c.conname AS constraint_name, \
70                    c.contype AS constraint_type, \
71                    array_agg(a.attname ORDER BY u.ord) AS columns \
72             FROM pg_constraint c \
73             JOIN pg_class t ON t.oid = c.conrelid \
74             JOIN pg_namespace n ON n.oid = t.relnamespace \
75             CROSS JOIN LATERAL unnest(c.conkey) WITH ORDINALITY AS u(attnum, ord) \
76             JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = u.attnum \
77             WHERE n.nspname = $1 AND t.relname = $2 \
78             GROUP BY c.conname, c.contype \
79             ORDER BY c.conname",
80        )
81        .bind(&self.schema)
82        .bind(&self.table)
83        .fetch_all(&self.pool)
84        .await
85        .map_err(pg_error)?;
86        let constraints = rows
87            .iter()
88            .map(|r| {
89                let type_char: String = r
90                    .try_get::<String, _>("constraint_type")
91                    .map_err(pg_error)?;
92                let constraint_type = match type_char.as_str() {
93                    "p" => "PRIMARY KEY",
94                    "f" => "FOREIGN KEY",
95                    "u" => "UNIQUE",
96                    "c" => "CHECK",
97                    "x" => "EXCLUSION",
98                    other => other,
99                }
100                .to_string();
101                Ok(ConstraintInfo {
102                    name: r
103                        .try_get::<String, _>("constraint_name")
104                        .map_err(pg_error)?,
105                    constraint_type,
106                    columns: r.try_get::<Vec<String>, _>("columns").map_err(pg_error)?,
107                })
108            })
109            .collect::<Result<Vec<_>, OperationError>>()?;
110        Ok(ListConstraintsOutput { constraints })
111    }
112}
113
114#[async_trait]
115impl Operation for ListConstraints {
116    fn kind(&self) -> &str {
117        "postgres"
118    }
119    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
120        to_value(&self.run(ctx).await?)
121    }
122    fn input(&self) -> Option<Value> {
123        Some(serde_json::json!({ "schema": self.schema, "table": self.table }))
124    }
125}
126
127impl TypedOperation for ListConstraints {
128    type Output = ListConstraintsOutput;
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[tokio::test]
136    async fn kind() {
137        let pool = PgPool::connect_lazy("postgres://localhost/test").unwrap();
138        let op = ListConstraints::new(pool, "public", "users");
139        assert_eq!(op.kind(), "postgres");
140    }
141}