supabase_rs 0.7.0

Lightweight Rust client for Supabase REST and GraphQL
Documentation
# RPC Implementation Design Document

## 1. Architectural Analysis

The current `supabase_rs` architecture follows a clear "Fluent Builder" pattern for database operations.
*   **Client**: `SupabaseClient` acts as the entry point, holding connection state (`reqwest::Client`), configuration (`url`, `key`), and schema information.
*   **Builders**: Operations like `select` return a `QueryBuilder`. This builder accumulates state (filters, sorts, pagination) in a `Query` struct without executing immediately.
*   **Execution**: The `.execute()` method triggers the HTTP request. For `select`, this is a `GET` request to `/rest/v1/{table}`.
*   **Query Construction**: The `Query` struct handles the logic of converting filters (e.g., `eq`, `gt`) into PostgREST-compatible URL query parameters.

**Gap Analysis for RPC**:
*   **Method**: RPC calls require `POST` requests, unlike the `GET` used for `select`.
*   **Parameters**: RPC arguments are sent in the JSON request body, not the URL.
*   **Filtering**: PostgREST allows filtering the *return value* of an RPC function (if it returns a table) using the same query parameters as `select`.
*   **Endpoint**: The endpoint format differs: `/rest/v1/rpc/{function_name}`.

## 2. RPC Design

To maintain consistency with the existing architecture, we will implement a `RpcBuilder` pattern.

*   **RpcBuilder**: Similar to `QueryBuilder`, but specialized for RPCs.
*   **State Separation**: It will manage two distinct sets of data:
    1.  **Function Arguments**: Passed in the body (JSON).
    2.  **Result Filters**: Passed in the URL query string (reusing the existing `Query` logic).
*   **Chaining**: The builder will support chaining filter methods (`eq`, `order`, etc.) which will apply to the *result* of the function call.

## 3. Module Structure

New files and modifications:

```text
src/
├── rpc.rs              # NEW: Implementation of RpcBuilder and execution logic
├── lib.rs              # MOD: Export rpc module and add rpc() method to SupabaseClient
└── tests/
    └── methods/
        └── rpc.rs      # NEW: Integration tests for RPC calls
```

### Integration
*   `src/lib.rs`: Add `pub mod rpc;` and `SupabaseClient::rpc(...)`.
*   `src/rpc.rs`: Will use `crate::query::Query` to reuse filter logic.

## 4. API Design

### `SupabaseClient` Extension

```rust
impl SupabaseClient {
    /// Calls a Postgres RPC function.
    ///
    /// # Arguments
    /// * `function_name` - The name of the RPC function to call.
    /// * `params` - The arguments to pass to the function (serializable to JSON).
    pub fn rpc<T>(&self, function_name: &str, params: T) -> RpcBuilder
    where
        T: serde::Serialize;
}
```

### `RpcBuilder` Interface

```rust
pub struct RpcBuilder {
    client: SupabaseClient,
    function_name: String,
    params: serde_json::Value,
    query: Query, // Reusing existing Query struct for filters
}

impl RpcBuilder {
    // Filter methods (delegating to self.query)
    pub fn eq(mut self, column: &str, value: &str) -> Self;
    pub fn gt(mut self, column: &str, value: &str) -> Self;
    // ... other standard filters ...
    
    // Modifiers
    pub fn order(mut self, column: &str, ascending: bool) -> Self;
    pub fn limit(mut self, n: usize) -> Self;

    // Execution
    pub async fn execute(self) -> Result<Vec<Value>, String>;
    pub async fn execute_void(self) -> Result<(), String>; // For functions returning void
    pub async fn execute_single(self) -> Result<Value, String>; // For scalar returns
}
```

## 5. Implementation Details

### `src/rpc.rs`

```rust
use crate::{SupabaseClient, query::{Query, QueryBuilder}};
use serde::Serialize;
use serde_json::{Value, json};

pub struct RpcBuilder {
    client: SupabaseClient,
    function_name: String,
    params: Value,
    query: Query,
}

impl RpcBuilder {
    pub fn new<T: Serialize>(client: SupabaseClient, function_name: &str, params: T) -> Self {
        Self {
            client,
            function_name: function_name.to_string(),
            params: serde_json::to_value(params).unwrap_or(json!({})),
            query: Query::new(),
        }
    }

    // Example filter delegation
    pub fn eq(mut self, column: &str, value: &str) -> Self {
        self.query.add_param(column, &format!("eq.{}", value));
        self
    }

    pub async fn execute(self) -> Result<Vec<Value>, String> {
        let endpoint = format!("{}/rest/v1/rpc/{}", self.client.url, self.function_name);
        let query_string = self.query.build();
        let url = if query_string.is_empty() {
            endpoint
        } else {
            format!("{}?{}", endpoint, query_string)
        };

        // Reuse headers logic from SupabaseClient (likely need to expose or duplicate)
        // ... header setup ...

        let response = self.client.client
            .post(&url)
            .headers(headers)
            .json(&self.params)
            .send()
            .await
            .map_err(|e| e.to_string())?;

        // Reuse response handling
        crate::success::handle_response(response).await
    }
}
```

## 6. Testing Strategy

### Unit Tests
*   **Builder Logic**: Verify that calling `.rpc(...).eq(...)` correctly populates both `params` (body) and `query` (URL params).
*   **Serialization**: Ensure parameters are correctly serialized to JSON.

### Integration Tests
*   **Setup**: Requires a Supabase instance with specific RPC functions defined (e.g., `echo_text`, `get_filtered_users`).
*   **Scenarios**:
    1.  **Void Return**: Call a function that performs an action but returns nothing.
    2.  **Scalar Return**: Call a function returning an integer or string.
    3.  **Table Return**: Call a function returning `SETOF users`.
    4.  **Filtering**: Call a table-returning function and apply `.eq()` and `.limit()`.
    5.  **Error**: Call a non-existent function or pass invalid parameters.

## 7. Type Generation Extension

The `src/type_gen.rs` module should be updated to support generating types for RPC functions.

*   **Discovery**: Query `information_schema.routines` where `routine_type = 'FUNCTION'` and `routine_schema = 'public'`.
*   **Parameters**: Join with `information_schema.parameters` to generate `Args` structs for each function.
*   **Output**:
    *   Generate a `Rpc` module or `rpc` submodule in `supabase_types.rs`.
    *   For each function, generate a helper struct:
        ```rust
        pub struct MyFunctionArgs {
            pub param1: String,
            pub param2: i32,
        }
        ```

## 8. Error Handling

*   **Network Errors**: handled by `reqwest` conversion.
*   **Postgres Errors**: PostgREST returns specific error JSON bodies (e.g., `PGRST200`). These should be parsed and returned as structured errors if possible, or clear strings.
*   **Argument Errors**: If serialization of `params` fails, return an immediate error before the request.

## 9. Performance Considerations

*   **Connection Pooling**: `RpcBuilder` reuses the `SupabaseClient`'s reqwest client, ensuring connection reuse.
*   **Allocation**: `Query` uses `Vec<(String, String)>`. For high-performance scenarios, we might consider using `Cow<str>` to reduce string cloning during builder chaining.
*   **Params**: `serde_json::Value` involves some overhead. The `rpc` method takes `T: Serialize`, which is efficient. The storage in `RpcBuilder` as `Value` allows flexibility but causes one serialization step. This is acceptable for typical usage.

## 10. Documentation Requirements

*   **Method Docs**: `SupabaseClient::rpc` must have examples showing both simple calls and filtered calls.
*   **Module Docs**: `src/rpc.rs` should explain that filters apply to the *result* of the function.
*   **README**: Add a section "Calling Stored Procedures".

### Example Documentation
```rust
/// Calls a Postgres RPC function.
///
/// # Examples
///
/// ```rust,no_run
/// // Call a function 'echo' with arguments
/// let result = client.rpc("echo", json!({"message": "hello"})).execute().await?;
///
/// // Call a function returning a table and filter the results
/// let users = client.rpc("get_active_users", json!({}))
///     .eq("role", "admin")
///     .limit(10)
///     .execute()
///     .await?;
/// ```
```