Crate supabase_wrappers
source ·Expand description
Wrappers is a development framework for Postgres Foreign Data Wrappers (FDW) based on pgrx.
Its goal is to make Postgres FDW development easier while keeping Rust language’s modern capabilities, such as high performance, strong types, and safety.
§Usage
Wrappers is a pgrx extension, please follow the installation steps to install pgrx
first.
After pgrx is installed, create your project using command like below,
$ cargo pgrx new my_project
And then change default Postgres version to pg14
or pg15
and add below dependencies to your project’s Cargo.toml
,
[features]
default = ["pg15"]
...
[dependencies]
pgrx = "=0.11.2"
supabase-wrappers = "0.1"
§Supported Types
For simplicity purpose, only a subset of pgrx types are supported currently. For example,
- bool
- f64
- i64
- String
- Timestamp
- JsonB
See the full supported types list in interface::Cell
. More types will be added in the future if needed or you can raise a request to us.
§Developing a FDW
The core interface is the interface::ForeignDataWrapper
trait which provides callback functions to be called by Postgres during different querying phases. For example,
- Query planning phase
- Scan phase
- begin_scan() required
- iter_scan() required
- re_scan()
- end_scan() required
- Modify phase
To give different functionalities to your FDW, you can choose different callback functions to implement. The required ones are begin_scan
, iter_scan
and end_scan
, all the others are optional. See Postgres FDW document for more details about FDW development.
The FDW implements interface::ForeignDataWrapper
trait must use wrappers_fdw
macro and implement a new()
initialization function. For example,
use std::collections::HashMap;
use pgrx::pg_sys::panic::ErrorReport;
use pgrx::PgSqlErrorCode;
use supabase_wrappers::prelude::*;
#[wrappers_fdw(
version = "0.1.1",
author = "Supabase",
website = "https://github.com/supabase/wrappers/tree/main/wrappers/src/fdw/helloworld_fdw",
error_type = "HelloWorldFdwError"
)]
pub struct HelloWorldFdw {
//row counter
row_cnt: i64,
// target column list
tgt_cols: Vec<Column>,
}
enum HelloWorldFdwError {}
impl From<HelloWorldFdwError> for ErrorReport {
fn from(_value: HelloWorldFdwError) -> Self {
ErrorReport::new(PgSqlErrorCode::ERRCODE_FDW_ERROR, "", "")
}
}
type HelloWorldFdwResult<T> = Result<T, HelloWorldFdwError>;
impl ForeignDataWrapper<HelloWorldFdwError> for HelloWorldFdw {
fn new(options: &HashMap<String, String>) -> HelloWorldFdwResult<Self> {
// 'options' is the key-value pairs defined in `CREATE SERVER` SQL, for example,
//
// create server my_helloworld_server
// foreign data wrapper wrappers_helloworld
// options (
// foo 'bar'
// );
//
// 'options' passed here will be a hashmap { 'foo' -> 'bar' }.
//
// You can do any initalization in this new() function, like saving connection
// info or API url in an variable, but don't do heavy works like database
// connection or API call.
Ok(Self {
row_cnt: 0,
tgt_cols: Vec::new(),
})
}
fn begin_scan(&mut self, quals: &[Qual], columns: &[Column], sorts: &[Sort], limit: &Option<Limit>, options: &HashMap<String, String>) -> HelloWorldFdwResult<()> {
// Do any initilization
Ok(())
}
fn iter_scan(&mut self, row: &mut Row) -> HelloWorldFdwResult<Option<()>> {
// Return None when done
Ok(None)
}
fn end_scan(&mut self) -> HelloWorldFdwResult<()> {
// Cleanup any resources
Ok(())
}
}
To develop a simple FDW supports basic query SELECT
, you need to implement begin_scan
, iter_scan
and end_scan
.
begin_scan
- called once at the beginning ofSELECT
iter_scan
- called for each row to be returned to Postgres, returnNone
to stop the scanend_scan
- called once at the end ofSELECT
Suppose the foreign table DDL is like below,
create foreign table hello (
id bigint,
col text
)
server my_helloworld_server
options (
foo 'bar'
);
Then we can implement interface::ForeignDataWrapper
trait like below,
use std::collections::HashMap;
use pgrx::pg_sys::panic::ErrorReport;
use pgrx::PgSqlErrorCode;
use supabase_wrappers::prelude::*;
pub(crate) struct HelloWorldFdw {
// row counter
row_cnt: i64,
// target column name list
tgt_cols: Vec<Column>,
}
enum HelloWorldFdwError {}
impl From<HelloWorldFdwError> for ErrorReport {
fn from(_value: HelloWorldFdwError) -> Self {
ErrorReport::new(PgSqlErrorCode::ERRCODE_FDW_ERROR, "", "")
}
}
impl ForeignDataWrapper<HelloWorldFdwError> for HelloWorldFdw {
fn new(options: &HashMap<String, String>) -> Result<Self, HelloWorldFdwError> {
Ok(Self {
row_cnt: 0,
tgt_cols: Vec::new(),
})
}
fn begin_scan(
&mut self,
_quals: &[Qual],
columns: &[Column],
_sorts: &[Sort],
_limit: &Option<Limit>,
_options: &HashMap<String, String>,
) -> Result<(), HelloWorldFdwError> {
// reset row count
self.row_cnt = 0;
// save a copy of target columns
self.tgt_cols = columns.to_vec();
Ok(())
}
fn iter_scan(&mut self, row: &mut Row) -> Result<Option<()>, HelloWorldFdwError> {
// this is called on each row and we only return one row here
if self.row_cnt < 1 {
// add values to row if they are in target column list
for tgt_col in &self.tgt_cols {
match tgt_col.name.as_str() {
"id" => row.push("id", Some(Cell::I64(self.row_cnt))),
"col" => row.push("col", Some(Cell::String("Hello world".to_string()))),
_ => {}
}
}
self.row_cnt += 1;
// return the 'Some(())' to Postgres and continue data scan
return Ok(Some(()));
}
// return 'None' to stop data scan
Ok(None)
}
fn end_scan(&mut self) -> Result<(), HelloWorldFdwError> {
// we do nothing here, but you can do things like resource cleanup and etc.
Ok(())
}
}
And that’s it. Now your FDW is ready to run,
$ cargo pgrx run
Then create the FDW and foreign table, and make a query on it,
create extension my_project;
create foreign data wrapper helloworld_wrapper
handler hello_world_fdw_handler
validator hello_world_fdw_validator;
create server my_helloworld_server
foreign data wrapper helloworld_wrapper;
create foreign table hello (
id bigint,
col text
)
server my_helloworld_server;
select * from hello;
id | col
----+-------------
0 | Hello world
(1 row)
§Pro Tips
You can use EXPLAIN
to check what have been pushed down. For example,
explain select * from hello where id = 1 order by col limit 1;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
Limit (cost=1.01..1.01 rows=1 width=40)
-> Sort (cost=1.01..1.01 rows=1 width=40)
Sort Key: col
-> Foreign Scan on hello (cost=0.00..1.00 rows=1 width=0)
Filter: (id = 1)
Wrappers: quals = [Qual { field: "id", operator: "=", value: Cell(I32(1)), use_or: false, param: None }]
Wrappers: tgts = [Column { name: "id", num: 1, type_oid: 20 }, Column { name: "col", num: 2, type_oid: 25 }]
Wrappers: sorts = [Sort { field: "col", field_no: 2, reversed: false, nulls_first: false, collate: None }]
Wrappers: limit = Some(Limit { count: 1, offset: 0 })
(9 rows)
§More FDW Examples
See more FDW examples which interact with RDBMS or RESTful API.
- HelloWorld: A demo FDW to show how to develop a baisc FDW.
- BigQuery: A FDW for Google BigQuery which supports data read and modify.
- Clickhouse: A FDW for ClickHouse which supports data read and modify.
- Stripe: A FDW for Stripe API which supports data read and modify.
- Firebase: A FDW for Google Firebase which supports data read only.
- Airtable: A FDW for Airtable API which supports data read only.
- S3: A FDW for AWS S3 which supports data read only.
- Logflare: A FDW for Logflare which supports data read only.
- Auth0: A FDW for Auth0 which supports data read only.
- SQL Server: A FDW for Microsoft SQL Server which supports data read only.
- Redis: A FDW for Redis which supports data read only.
Modules§
- Provides interface types and trait to develop Postgres foreign data wrapper
- The prelude includes all necessary imports to make Wrappers work
- Helper functions for working with Wrappers
Type Aliases§
- PgBox’ed
FdwRoutine
, used infdw_routine
Attribute Macros§
- Create necessary handler, validator and meta functions for foreign data wrapper