Skip to main content

ironflow_ops_postgres/schema/
indexes.rs

1//! Index 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/// An index description.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct IndexInfo {
15    /// Index name.
16    pub name: String,
17    /// Whether the index enforces uniqueness.
18    pub is_unique: bool,
19    /// Whether this is the primary key index.
20    pub is_primary: bool,
21    /// Index definition (the `CREATE INDEX` statement).
22    pub definition: String,
23}
24
25/// Output of [`ListIndexes`].
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ListIndexesOutput {
28    /// Indexes on the table.
29    pub indexes: Vec<IndexInfo>,
30}
31
32/// List the indexes on a table.
33///
34/// # Examples
35///
36/// ```no_run
37/// use ironflow_ops_postgres::schema::indexes::ListIndexes;
38/// use ironflow_core::operation::Operation;
39///
40/// # fn example(pool: sqlx::PgPool) {
41/// let op = ListIndexes::new(pool, "public", "users");
42/// assert_eq!(op.kind(), "postgres");
43/// # }
44/// ```
45pub struct ListIndexes {
46    pool: PgPool,
47    schema: String,
48    table: String,
49}
50
51impl ListIndexes {
52    /// Create a new list-indexes operation.
53    pub fn new(pool: PgPool, schema: impl Into<String>, table: impl Into<String>) -> Self {
54        Self {
55            pool,
56            schema: schema.into(),
57            table: table.into(),
58        }
59    }
60
61    /// Execute and return a typed result.
62    ///
63    /// # Errors
64    ///
65    /// Returns [`OperationError::External`] on connection errors.
66    pub async fn run(&self, _ctx: &OperationContext) -> Result<ListIndexesOutput, OperationError> {
67        let rows = sqlx::query(
68            "SELECT i.relname AS index_name, \
69                    ix.indisunique AS is_unique, \
70                    ix.indisprimary AS is_primary, \
71                    pg_get_indexdef(ix.indexrelid) AS definition \
72             FROM pg_index ix \
73             JOIN pg_class i ON i.oid = ix.indexrelid \
74             JOIN pg_class t ON t.oid = ix.indrelid \
75             JOIN pg_namespace n ON n.oid = t.relnamespace \
76             WHERE n.nspname = $1 AND t.relname = $2 \
77             ORDER BY i.relname",
78        )
79        .bind(&self.schema)
80        .bind(&self.table)
81        .fetch_all(&self.pool)
82        .await
83        .map_err(pg_error)?;
84        let indexes = rows
85            .iter()
86            .map(|r| {
87                Ok(IndexInfo {
88                    name: r.try_get::<String, _>("index_name").map_err(pg_error)?,
89                    is_unique: r.try_get::<bool, _>("is_unique").map_err(pg_error)?,
90                    is_primary: r.try_get::<bool, _>("is_primary").map_err(pg_error)?,
91                    definition: r.try_get::<String, _>("definition").map_err(pg_error)?,
92                })
93            })
94            .collect::<Result<Vec<_>, OperationError>>()?;
95        Ok(ListIndexesOutput { indexes })
96    }
97}
98
99#[async_trait]
100impl Operation for ListIndexes {
101    fn kind(&self) -> &str {
102        "postgres"
103    }
104    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
105        to_value(&self.run(ctx).await?)
106    }
107    fn input(&self) -> Option<Value> {
108        Some(serde_json::json!({ "schema": self.schema, "table": self.table }))
109    }
110}
111
112impl TypedOperation for ListIndexes {
113    type Output = ListIndexesOutput;
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[tokio::test]
121    async fn kind() {
122        let pool = PgPool::connect_lazy("postgres://localhost/test").unwrap();
123        let op = ListIndexes::new(pool, "public", "users");
124        assert_eq!(op.kind(), "postgres");
125    }
126}