supabase_rs 0.7.0

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

## 1. Architectural Integration

The RPC implementation will integrate into the existing `supabase_rs` architecture by adding a new `RpcBuilder` that parallels the existing `QueryBuilder`. This ensures a consistent developer experience while handling the unique requirements of PostgREST RPC calls (POST method, body parameters, and URL filtering).

### Integration Points
1.  **`SupabaseClient`**: Extended with a `rpc()` method serving as the entry point.
2.  **`RpcBuilder`**: A new builder struct in `src/rpc.rs` that manages the request lifecycle.
3.  **`Query`**: Reused from `src/query.rs` to handle response filtering (filtering the return set of a function).
4.  **`Headers`**: Enhanced to support schema switching via `Content-Profile` and `Accept-Profile`.

## 2. API Specification

### `SupabaseClient` Extension
We will add the `rpc` method to `SupabaseClient` in `src/lib.rs`.

```rust
// src/lib.rs

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. Can be a struct, map, or `json!({})`.
    ///
    /// # Returns
    /// Returns a `RpcBuilder` for further chaining (filtering) or execution.
    pub fn rpc<T>(&self, function_name: &str, params: T) -> crate::rpc::RpcBuilder
    where
        T: serde::Serialize,
    {
        crate::rpc::RpcBuilder::new(self.clone(), function_name, params)
    }
}
```

### `RpcBuilder` Structure
Located in `src/rpc.rs`, this builder manages the state.

```rust
// src/rpc.rs

pub struct RpcBuilder {
    client: SupabaseClient,
    function_name: String,
    params: serde_json::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(),
        }
    }

    // ... filters ...
    // ... execution methods ...
}
```

## 3. Header Management

RPC calls require specific headers to handle schemas correctly. PostgREST uses:
*   `Content-Profile`: To specify the schema for the input arguments (request body).
*   `Accept-Profile`: To specify the schema for the return value (response body).

We must update `src/request/headers.rs` or handle this in `RpcBuilder::execute`. Since `SupabaseClient` holds the `schema` (defaulting to "public"), we use this.

```rust
// Inside RpcBuilder::execute()

let mut headers = self.client.headers.clone(); // Assuming headers are accessible or re-created

// 1. Content-Type is always application/json
headers.insert("Content-Type", "application/json");

// 2. Handle Schema
if self.client.schema != "public" {
    // Tell PostgREST to search for the function/types in this schema
    headers.insert("Content-Profile", &self.client.schema);
    // Tell PostgREST to return results from this schema
    headers.insert("Accept-Profile", &self.client.schema);
}

// 3. RPC Parameter Format
// Default PostgREST behavior expects a JSON object for parameters.
// If we ever need single unnamed parameter support, we'd use:
// headers.insert("Prefer", "params=single-object"); 
```

## 4. Endpoint Construction

We need a dedicated method to construct the RPC endpoint, handling the `rest/v1` prefix and the `rpc/` segment.

```rust
// src/lib.rs

impl SupabaseClient {
    pub(crate) fn rpc_endpoint(&self, function_name: &str) -> String {
        let dont_use_rest_v1: bool = std::env::var("SUPABASE_RS_DONT_REST_V1_URL")
            .map(|val| val.to_lowercase() == "true")
            .unwrap_or(false);

        let base = if dont_use_rest_v1 {
            format!("{}/rpc", self.url)
        } else {
            format!("{}/rest/v1/rpc", self.url)
        };

        format!("{}/{}", base, function_name)
    }
}
```

## 5. Parameter Handling System

To minimize serialization overhead while keeping the API flexible:
1.  **Input**: The `rpc` method takes `T: Serialize`.
2.  **Storage**: We convert to `serde_json::Value` immediately in `new()`. This allows `RpcBuilder` to be non-generic, simplifying the type signature for chaining.
    *   *Optimization Note*: While this causes one serialization, it avoids complex generic bounds on the Builder which would complicate `Filter` trait implementations or delegation.
3.  **Sending**: The `reqwest` client takes `&Value` directly.

## 6. Response Type System

RPC functions can return:
1.  `void`: No content.
2.  `scalar`: A single value (int, string, bool).
3.  `row`: A single object.
4.  `setof`: An array of objects.

We provide typed execution methods:

```rust
impl RpcBuilder {
    /// Default execution expecting a list of results (SETOF records)
    pub async fn execute(self) -> Result<Vec<Value>, ErrorTypes> {
        // ... implementation ...
    }

    /// For functions returning a single scalar or object
    pub async fn execute_single(self) -> Result<Value, ErrorTypes> {
         // ... sets Accept: application/vnd.pgrst.object+json if needed, or parses result ...
    }

    /// For functions returning void
    pub async fn execute_void(self) -> Result<(), ErrorTypes> {
         // ... implementation checks for 204 No Content ...
    }
}
```

## 7. Error Handling Integration

We will use the existing `crate::errors::ErrorTypes` and `Result`.

*   **PostgREST Errors**: Map HTTP 404 (Function not found) and 500 (SQL Error) to `ErrorTypes`.
*   **Parsing**: PostgREST returns JSON error bodies (e.g., `{"code": "...", "message": "..."}`). We should attempt to parse this into a structured error message within `ReqwestError` or a new `PostgresError` variant if we decide to expand `ErrorTypes`. For now, we'll stick to stringifying the error body into the `Result`.

```rust
// In execute()
let status = response.status();
if !status.is_success() {
    let error_body = response.text().await.unwrap_or_default();
    return Err(ErrorTypes::ReqwestError(
        // Construct a meaningful error
    ));
}
```

## 8. Type Generation Extension

We will extend `src/type_gen.rs` to generate types for RPC function arguments.

**SQL Query:**
```sql
SELECT
    r.routine_name,
    p.parameter_name,
    p.data_type,
    p.ordinal_position,
    p.parameter_mode
FROM information_schema.routines r
JOIN information_schema.parameters p 
    ON r.specific_name = p.specific_name
WHERE r.routine_type = 'FUNCTION' 
  AND r.routine_schema = 'public' -- or configurable
ORDER BY r.routine_name, p.ordinal_position;
```

**Rust Output Strategy:**
Generate a `pub mod rpc` inside `supabase_types.rs`.
For a function `create_user(name text, age int)`, generate:

```rust
pub mod rpc {
    use serde::Serialize;

    #[derive(Serialize)]
    pub struct CreateUserArgs {
        pub name: String,
        pub age: i32,
    }
}
```

## 9. Testing Infrastructure

### Setup Script (`src/tests/setup_rpc.sql`)
```sql
-- Void return
CREATE OR REPLACE FUNCTION test_void_func() RETURNS void AS $$
BEGIN
    -- do nothing
END;
$$ LANGUAGE plpgsql;

-- Scalar return
CREATE OR REPLACE FUNCTION test_echo(val text) RETURNS text AS $$
BEGIN
    RETURN val;
END;
$$ LANGUAGE plpgsql;

-- Set return
CREATE OR REPLACE FUNCTION test_get_users() RETURNS SETOF users AS $$
BEGIN
    RETURN QUERY SELECT * FROM users;
END;
$$ LANGUAGE plpgsql;
```

### Integration Test (`src/tests/methods/rpc.rs`)
```rust
#[tokio::test]
async fn test_rpc_echo() {
    let client = create_client();
    let res = client.rpc("test_echo", json!({"val": "hello"}))
        .execute_single()
        .await
        .unwrap();
    assert_eq!(res.as_str().unwrap(), "hello");
}
```

## 10. Schema Support

The implementation must strictly respect `SupabaseClient.schema`.
1.  **Header Injection**: As detailed in section 3, inject `Content-Profile` and `Accept-Profile`.
2.  **Type Gen**: The generator must accept a schema parameter to inspect `information_schema.routines` for that specific schema.

## 11. Performance Optimizations

1.  **Connection Pooling**: Inherited from `SupabaseClient` (reqwest).
2.  **String Cloning**: `RpcBuilder` stores `String` for function name. This is negligible.
3.  **Serialization**: We avoid double serialization in the request body by taking `&Value` in the final `reqwest` call.

## 12. Feature Flag Strategy

Add a `rpc` feature to `Cargo.toml`.

```toml
[features]
default = []
rpc = []
```

Conditionally compile the `rpc` module in `lib.rs`:

```rust
#[cfg(feature = "rpc")]
pub mod rpc;

impl SupabaseClient {
    #[cfg(feature = "rpc")]
    pub fn rpc(...) { ... }
}
```

## 13. Migration Considerations

*   **Non-Breaking**: Adding `rpc` is additive.
*   **Deprecation**: None required.
*   **Compatibility**: Works with standard PostgREST setup. Requires no changes to existing `select`, `insert`, etc.