Wrappers is a development framework for PostgreSQL Foreign Data Wrappers (FDWs), written in Rust. It enables querying external data sources (APIs, databases, files) as if they were regular PostgreSQL tables. The project is maintained by Supabase.
Documentation: https://fdw.dev/ | API Docs: https://docs.rs/supabase-wrappers
wrappers/
├── supabase-wrappers/ # Core FDW framework library (crates.io: supabase-wrappers)
├── supabase-wrappers-macros/ # Procedural macros (#[wrappers_fdw])
├── wrappers/ # Native FDW implementations (PostgreSQL extension)
│ └── src/fdw/ # Individual FDW implementations
├── wasm-wrappers/ # WebAssembly-based FDW implementations (separate workspace)
│ └── fdw/ # Individual Wasm FDW crates
└── docs/ # MkDocs documentation site
The project uses Cargo workspaces with the following structure:
- Main workspace (
Cargo.toml): Containssupabase-wrappers,supabase-wrappers-macros, andwrappers - Wasm workspace (
wasm-wrappers/fdw/Cargo.toml): Separate workspace for Wasm FDWs (excluded from main)
Rust version: 1.88.0 (specified in workspace.package)
pgrx version: 0.16.1 (PostgreSQL extension framework)
The core library providing the ForeignDataWrapper trait (supabase-wrappers/src/interface.rs):
pub trait ForeignDataWrapper<E: Into<ErrorReport>> {
// Required methods
fn new(server: ForeignServer) -> Result<Self, E>;
fn begin_scan(&mut self, quals: &[Qual], columns: &[Column], sorts: &[Sort], limit: &Option<Limit>, options: &HashMap<String, String>) -> Result<(), E>;
fn iter_scan(&mut self, row: &mut Row) -> Result<Option<()>, E>;
fn end_scan(&mut self) -> Result<(), E>;
// Optional methods for modification
fn begin_modify(&mut self, options: &HashMap<String, String>) -> Result<(), E>;
fn insert(&mut self, row: &Row) -> Result<(), E>;
fn update(&mut self, rowid: &Cell, new_row: &Row) -> Result<(), E>;
fn delete(&mut self, rowid: &Cell) -> Result<(), E>;
fn end_modify(&mut self) -> Result<(), E>;
// Optional methods
fn re_scan(&mut self) -> Result<(), E>;
fn get_rel_size(...) -> Result<(i64, i32), E>;
fn import_foreign_schema(...) -> Result<Vec<String>, E>;
fn validator(options: Vec<Option<String>>, catalog: Option<Oid>) -> Result<(), E>;
}Cell: Enum representing data values (Bool, I8-I64, F32-F64, String, Date, Timestamp, Json, Uuid, arrays)Row: Collection of column names and cellsColumn: Column metadata (name, number, type_oid)Qual: WHERE clause predicate (field, operator, value, use_or)Sort: ORDER BY specificationLimit: LIMIT/OFFSET values
Provides the #[wrappers_fdw] attribute macro that generates:
<name>_fdw_handler()- FDW handler entry point<name>_fdw_validator()- Option validation function<name>_fdw_meta()- Metadata function
Usage:
#[wrappers_fdw(
version = "0.1.0",
author = "Supabase",
website = "https://example.com",
error_type = "MyFdwError" // Required
)]
pub struct MyFdw { ... }| FDW | Feature Flag | Supports Write |
|---|---|---|
| BigQuery | bigquery_fdw |
Yes |
| ClickHouse | clickhouse_fdw |
Yes |
| Stripe | stripe_fdw |
Yes |
| S3 Vectors | s3vectors_fdw |
Yes |
| S3 | s3_fdw |
No |
| Firebase | firebase_fdw |
No |
| Airtable | airtable_fdw |
No |
| Auth0 | auth0_fdw |
No |
| AWS Cognito | cognito_fdw |
No |
| DuckDB | duckdb_fdw |
No |
| Apache Iceberg | iceberg_fdw |
No |
| Logflare | logflare_fdw |
No |
| Redis | redis_fdw |
No |
| SQL Server | mssql_fdw |
No |
| HelloWorld | helloworld_fdw |
No (demo) |
Cal.com, Calendly, Clerk, Cloudflare D1, HubSpot, Infura, Notion, Orb, Paddle, Shopify, Slack, Snowflake
# Install Rust toolchain
rustup install 1.88.0
rustup default 1.88.0
# Install pgrx
cargo install --locked cargo-pgrx --version 0.16.1
# Initialize pgrx with PostgreSQL
cargo pgrx init --pg15 /usr/lib/postgresql/15/bin/pg_config
# For Wasm development
cargo install --locked cargo-component --version 0.21.1
rustup target add wasm32-unknown-unknown# Build native FDWs
cd wrappers
cargo build --features "native_fdws pg15"
# Build specific FDW
cargo build --features "stripe_fdw pg15"
# Build Wasm FDWs
cd wasm-wrappers/fdw
cargo component build --release --target wasm32-unknown-unknowncd wrappers
cargo pgrx run pg15 --features stripe_fdwThen in psql:
create extension if not exists wrappers;
create foreign data wrapper stripe_wrapper
handler stripe_fdw_handler
validator stripe_fdw_validator;# Start test containers
cd wrappers
docker compose -f .ci/docker-compose-native.yaml up -d
# Run all native FDW tests
cargo pgrx test --features "native_fdws pg15"
# Run specific FDW tests
cargo pgrx test --features "stripe_fdw pg15"
# Run Wasm FDW tests (requires building Wasm packages first)
docker compose -f .ci/docker-compose-wasm.yaml up -d
cargo pgrx test --features "wasm_fdw pg15"# Format check
cargo fmt --check
# Clippy (must pass with -D warnings)
RUSTFLAGS="-D warnings" cargo clippy --all --tests --no-deps --features native_fdws,helloworld_fdwcargo pgrx install --pg-config /path/to/pg_config --features stripe_fdwEach FDW defines a custom error type:
use thiserror::Error;
#[derive(Error, Debug)]
enum MyFdwError {
#[error("API error: {0}")]
ApiError(String),
#[error("Invalid option: {0}")]
InvalidOption(String),
}
impl From<MyFdwError> for ErrorReport {
fn from(e: MyFdwError) -> Self {
ErrorReport::new(PgSqlErrorCode::ERRCODE_FDW_ERROR, e.to_string(), "")
}
}use supabase_wrappers::prelude::*;
#[wrappers_fdw(
version = "0.1.0",
author = "Author",
website = "https://example.com",
error_type = "MyFdwError"
)]
pub struct MyFdw {
// State fields
client: Option<Client>,
rows: Vec<Row>,
row_idx: usize,
}
impl ForeignDataWrapper<MyFdwError> for MyFdw {
fn new(server: ForeignServer) -> Result<Self, MyFdwError> {
// Initialize from server options
Ok(Self { client: None, rows: vec![], row_idx: 0 })
}
fn begin_scan(
&mut self,
quals: &[Qual],
columns: &[Column],
sorts: &[Sort],
limit: &Option<Limit>,
options: &HashMap<String, String>,
) -> Result<(), MyFdwError> {
// Fetch data, apply pushdown
self.row_idx = 0;
Ok(())
}
fn iter_scan(&mut self, row: &mut Row) -> Result<Option<()>, MyFdwError> {
if self.row_idx < self.rows.len() {
row.replace_with(self.rows[self.row_idx].clone());
self.row_idx += 1;
Ok(Some(()))
} else {
Ok(None)
}
}
fn end_scan(&mut self) -> Result<(), MyFdwError> {
self.rows.clear();
Ok(())
}
}Each native FDW follows this structure:
wrappers/src/fdw/<name>_fdw/
├── mod.rs # Error types and module exports
├── <name>_fdw.rs # Main implementation
└── tests.rs # pgrx tests
FDWs receive pushdown hints through begin_scan:
quals: WHERE predicates to filter at sourcesorts: ORDER BY to sort at sourcelimit: LIMIT/OFFSET to limit at source
Use Qual::deparse() to convert to SQL-like strings.
Supported via feature flags: pg13, pg14, pg15 (default), pg16, pg17, pg18
GitHub Actions workflows in .github/workflows/:
test_wrappers.yml: Tests native and Wasm FDWstest_supabase_wrappers.yml: Tests core frameworkrelease.yml: Releases native FDWsrelease_wasm_fdw.yml: Releases Wasm FDWscoverage.yml: Code coveragedocs.yml: Documentation deployment
- Create directory
wrappers/src/fdw/<name>_fdw/ - Add feature flag in
wrappers/Cargo.toml - Add to
native_fdwsfeature list - Implement
ForeignDataWrappertrait - Add tests in
tests.rs - Add documentation in
docs/catalog/
Use pgrx notice! macro for debug output:
use pgrx::notice;
notice!("Debug: {:?}", value);Options come from CREATE SERVER and CREATE FOREIGN TABLE:
fn begin_scan(&mut self, ..., options: &HashMap<String, String>) {
let table_name = options.get("table").unwrap_or(&"default".to_string());
}- Native FDW contributions are not currently accepted until API stabilizes (v1.0)
- Wasm FDWs are preferred for community contributions
- Materialized views with foreign tables may cause backup restoration issues
- Use
rowid_columntable option to enable INSERT/UPDATE/DELETE operations