Skip to main content

postgrest_parser/sql/
rpc.rs

1use crate::ast::{ResolvedTable, RpcParams};
2use crate::error::SqlError;
3use crate::sql::{QueryBuilder, QueryResult};
4
5impl QueryBuilder {
6    /// Builds an RPC (function call) query with schema-qualified function name
7    ///
8    /// Generates: SELECT * FROM "schema"."function_name"(arg1 := $1, arg2 := $2)
9    /// PostgREST allows filtering, ordering, and pagination of function results
10    pub fn build_rpc(
11        &mut self,
12        resolved_table: &ResolvedTable,
13        params: &RpcParams,
14    ) -> Result<QueryResult, SqlError> {
15        self.tables.push(params.function_name.clone());
16
17        // SELECT clause (use returning if specified, otherwise *)
18        if let Some(ref returning) = params.returning {
19            self.build_select_clause(returning)?;
20        } else {
21            self.sql.push_str("SELECT *");
22        }
23
24        // FROM schema.function_name(args)
25        self.sql.push_str(" FROM ");
26        self.sql.push_str(&format!(
27            "\"{}\".\"{}\"(",
28            resolved_table.schema, resolved_table.name
29        ));
30
31        // Build named arguments in deterministic order
32        if !params.args.is_empty() {
33            let mut sorted_args: Vec<(&String, &serde_json::Value)> = params.args.iter().collect();
34            sorted_args.sort_by_key(|(k, _)| *k);
35
36            for (i, (name, value)) in sorted_args.iter().enumerate() {
37                if i > 0 {
38                    self.sql.push_str(", ");
39                }
40                let param_placeholder = self.add_param((*value).clone());
41                self.sql
42                    .push_str(&format!("\"{}\" := {}", name, param_placeholder));
43            }
44        }
45
46        self.sql.push(')');
47
48        // WHERE clause for filtering function results
49        if !params.filters.is_empty() {
50            self.build_where_clause(&params.filters)?;
51        }
52
53        // ORDER BY clause
54        if !params.order.is_empty() {
55            self.build_order_clause(&params.order)?;
56        }
57
58        // LIMIT and OFFSET
59        self.build_limit_offset(params.limit, params.offset)?;
60
61        Ok(QueryResult {
62            query: self.sql.clone(),
63            params: self.params.clone(),
64            tables: self.tables.clone(),
65        })
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use crate::ast::{LogicCondition, ResolvedTable, RpcParams};
73    use crate::parser::{parse_filter, parse_order};
74    use serde_json::Value;
75    use std::collections::HashMap;
76
77    #[test]
78    fn test_build_rpc_simple() {
79        let mut builder = QueryBuilder::new();
80        let resolved = ResolvedTable::new("public", "get_user_profile");
81
82        let mut args = HashMap::new();
83        args.insert("user_id".to_string(), Value::Number(123.into()));
84
85        let params = RpcParams::new("get_user_profile", args);
86
87        let result = builder.build_rpc(&resolved, &params).unwrap();
88
89        assert_eq!(
90            result.query,
91            r#"SELECT * FROM "public"."get_user_profile"("user_id" := $1)"#
92        );
93        assert_eq!(result.params.len(), 1);
94    }
95
96    #[test]
97    fn test_build_rpc_no_args() {
98        let mut builder = QueryBuilder::new();
99        let resolved = ResolvedTable::new("api", "health_check");
100
101        let params = RpcParams::new("health_check", HashMap::new());
102
103        let result = builder.build_rpc(&resolved, &params).unwrap();
104
105        assert_eq!(result.query, r#"SELECT * FROM "api"."health_check"()"#);
106        assert!(result.params.is_empty());
107    }
108
109    #[test]
110    fn test_build_rpc_multiple_args() {
111        let mut builder = QueryBuilder::new();
112        let resolved = ResolvedTable::new("public", "find_employees");
113
114        let mut args = HashMap::new();
115        args.insert("department".to_string(), Value::String("IT".to_string()));
116        args.insert("min_salary".to_string(), Value::Number(50000.into()));
117
118        let params = RpcParams::new("find_employees", args);
119
120        let result = builder.build_rpc(&resolved, &params).unwrap();
121
122        assert!(result.query.contains(r#""department" := $1"#));
123        assert!(result.query.contains(r#""min_salary" := $2"#));
124        assert_eq!(result.params.len(), 2);
125    }
126
127    #[test]
128    fn test_build_rpc_with_filters() {
129        let mut builder = QueryBuilder::new();
130        let resolved = ResolvedTable::new("public", "get_recent_posts");
131
132        let filter = parse_filter("status", "eq.published").unwrap();
133        let params = RpcParams::new("get_recent_posts", HashMap::new())
134            .with_filters(vec![LogicCondition::Filter(filter)]);
135
136        let result = builder.build_rpc(&resolved, &params).unwrap();
137
138        assert!(result.query.contains("WHERE"));
139        assert!(result.query.contains(r#""status" = $1"#));
140    }
141
142    #[test]
143    fn test_build_rpc_with_order() {
144        let mut builder = QueryBuilder::new();
145        let resolved = ResolvedTable::new("public", "get_posts");
146
147        let params = RpcParams::new("get_posts", HashMap::new())
148            .with_order(parse_order("created_at.desc").unwrap());
149
150        let result = builder.build_rpc(&resolved, &params).unwrap();
151
152        assert!(result.query.contains("ORDER BY"));
153        assert!(result.query.contains(r#""created_at" DESC"#));
154    }
155
156    #[test]
157    fn test_build_rpc_with_limit_offset() {
158        let mut builder = QueryBuilder::new();
159        let resolved = ResolvedTable::new("public", "list_users");
160
161        let params = RpcParams::new("list_users", HashMap::new())
162            .with_limit(10)
163            .with_offset(20);
164
165        let result = builder.build_rpc(&resolved, &params).unwrap();
166
167        assert!(result.query.contains("LIMIT $1"));
168        assert!(result.query.contains("OFFSET $2"));
169        assert_eq!(result.params.len(), 2);
170    }
171
172    #[test]
173    fn test_build_rpc_complex() {
174        let mut builder = QueryBuilder::new();
175        let resolved = ResolvedTable::new("api", "search_products");
176
177        let mut args = HashMap::new();
178        args.insert("query".to_string(), Value::String("laptop".to_string()));
179        args.insert("min_price".to_string(), Value::Number(500.into()));
180
181        let filter = parse_filter("in_stock", "eq.true").unwrap();
182        let params = RpcParams::new("search_products", args)
183            .with_filters(vec![LogicCondition::Filter(filter)])
184            .with_order(parse_order("price.asc").unwrap())
185            .with_limit(20);
186
187        let result = builder.build_rpc(&resolved, &params).unwrap();
188
189        assert!(result.query.contains(r#"FROM "api"."search_products"("#));
190        assert!(result.query.contains("WHERE"));
191        assert!(result.query.contains("ORDER BY"));
192        assert!(result.query.contains("LIMIT"));
193        assert!(result.params.len() >= 3);
194    }
195}