supabase_wrappers/lib.rs
1//! Wrappers is a development framework for Postgres Foreign Data Wrappers ([FDW](https://wiki.postgresql.org/wiki/Foreign_data_wrappers)) based on [pgrx](https://github.com/tcdi/pgrx).
2//!
3//! Its goal is to make Postgres FDW development easier while keeping Rust language's modern capabilities, such as high performance, strong types, and safety.
4//!
5//! # Usage
6//!
7//! Wrappers is a pgrx extension, please follow the [installation steps](https://github.com/tcdi/pgrx#system-requirements) to install `pgrx` first.
8//!
9//! After pgrx is installed, create your project using command like below,
10//!
11//! ```bash
12//! $ cargo pgrx new my_project
13//! ```
14//!
15//! And then change default Postgres version to `pg14` or `pg15` and add below dependencies to your project's `Cargo.toml`,
16//!
17//! ```toml
18//! [features]
19//! default = ["pg15"]
20//! ...
21//!
22//! [dependencies]
23//! pgrx = "=0.11.2"
24//! supabase-wrappers = "0.1"
25//! ```
26//!
27//! # Supported Types
28//!
29//! For simplicity purpose, only a subset of [pgrx types](https://github.com/tcdi/pgrx#mapping-of-postgres-types-to-rust) are supported currently. For example,
30//!
31//! - bool
32//! - f64
33//! - i64
34//! - String
35//! - Timestamp
36//! - JsonB
37//!
38//! 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](https://github.com/supabase/wrappers/issues) to us.
39//!
40//! # Developing a FDW
41//!
42//! The core interface is the [`interface::ForeignDataWrapper`] trait which provides callback functions to be called by Postgres during different querying phases. For example,
43//!
44//! - Query planning phase
45//! - [get_rel_size()](`interface::ForeignDataWrapper#method.get_rel_size`)
46//! - Scan phase
47//! - [begin_scan()](`interface::ForeignDataWrapper#tymethod.begin_scan`) *required*
48//! - [iter_scan()](`interface::ForeignDataWrapper#tymethod.iter_scan`) *required*
49//! - [re_scan()](`interface::ForeignDataWrapper#method.re_scan`)
50//! - [end_scan()](`interface::ForeignDataWrapper#tymethod.end_scan`) *required*
51//! - Modify phase
52//! - [begin_modify()](`interface::ForeignDataWrapper#method.begin_modify`)
53//! - [insert()](`interface::ForeignDataWrapper#method.insert`)
54//! - [update()](`interface::ForeignDataWrapper#method.update`)
55//! - [delete()](`interface::ForeignDataWrapper#method.delete`)
56//! - [end_modify()](`interface::ForeignDataWrapper#method.end_modify`)
57//!
58//! 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](https://www.postgresql.org/docs/current/fdw-callbacks.html) for more details about FDW development.
59//!
60//! The FDW implements [`interface::ForeignDataWrapper`] trait must use [`wrappers_fdw`] macro and implement a `new()` initialization function. For example,
61//!
62//! ```rust,no_run
63//! # mod wrapper {
64//! use std::collections::HashMap;
65//! use pgrx::pg_sys::panic::ErrorReport;
66//! use pgrx::PgSqlErrorCode;
67//! use supabase_wrappers::prelude::*;
68//! #[wrappers_fdw(
69//! version = "0.1.1",
70//! author = "Supabase",
71//! website = "https://github.com/supabase/wrappers/tree/main/wrappers/src/fdw/helloworld_fdw",
72//! error_type = "HelloWorldFdwError"
73//! )]
74//! pub struct HelloWorldFdw {
75//! //row counter
76//! row_cnt: i64,
77//!
78//! // target column list
79//! tgt_cols: Vec<Column>,
80//! }
81//!
82//! enum HelloWorldFdwError {}
83//!
84//! impl From<HelloWorldFdwError> for ErrorReport {
85//! fn from(_value: HelloWorldFdwError) -> Self {
86//! ErrorReport::new(PgSqlErrorCode::ERRCODE_FDW_ERROR, "", "")
87//! }
88//! }
89//!
90//! type HelloWorldFdwResult<T> = Result<T, HelloWorldFdwError>;
91//!
92//! impl ForeignDataWrapper<HelloWorldFdwError> for HelloWorldFdw {
93//! fn new(server: ForeignServer) -> HelloWorldFdwResult<Self> {
94//! // 'server.options' is the key-value pairs defined in `CREATE SERVER` SQL, for example,
95//! //
96//! // create server my_helloworld_server
97//! // foreign data wrapper wrappers_helloworld
98//! // options (
99//! // foo 'bar'
100//! // );
101//! //
102//! // 'options' passed here will be a hashmap { 'foo' -> 'bar' }.
103//! //
104//! // You can do any initalization in this new() function, like saving connection
105//! // info or API url in an variable, but don't do heavy works like database
106//! // connection or API call.
107//! Ok(Self {
108//! row_cnt: 0,
109//! tgt_cols: Vec::new(),
110//! })
111//! }
112//!
113//! fn begin_scan(&mut self, quals: &[Qual], columns: &[Column], sorts: &[Sort], limit: &Option<Limit>, options: &HashMap<String, String>) -> HelloWorldFdwResult<()> {
114//! // Do any initilization
115//! Ok(())
116//! }
117//!
118//! fn iter_scan(&mut self, row: &mut Row) -> HelloWorldFdwResult<Option<()>> {
119//! // Return None when done
120//! Ok(None)
121//! }
122//!
123//! fn end_scan(&mut self) -> HelloWorldFdwResult<()> {
124//! // Cleanup any resources
125//! Ok(())
126//! }
127//! }
128//! # }
129//! ```
130//!
131//! To develop a simple FDW supports basic query `SELECT`, you need to implement `begin_scan`, `iter_scan` and `end_scan`.
132//!
133//! - `begin_scan` - called once at the beginning of `SELECT`
134//! - `iter_scan` - called for each row to be returned to Postgres, return `None` to stop the scan
135//! - `end_scan` - called once at the end of `SELECT`
136//!
137//! Suppose the foreign table DDL is like below,
138//!
139//! ```sql
140//! create foreign table hello (
141//! id bigint,
142//! col text
143//! )
144//! server my_helloworld_server
145//! options (
146//! foo 'bar'
147//! );
148//! ```
149//!
150//! Then we can implement [`interface::ForeignDataWrapper`] trait like below,
151//!
152//! ```rust,no_run
153//! use std::collections::HashMap;
154//! use pgrx::pg_sys::panic::ErrorReport;
155//! use pgrx::PgSqlErrorCode;
156//! use supabase_wrappers::prelude::*;
157//!
158//! pub(crate) struct HelloWorldFdw {
159//! // row counter
160//! row_cnt: i64,
161//!
162//! // target column name list
163//! tgt_cols: Vec<Column>,
164//! }
165//!
166//! enum HelloWorldFdwError {}
167//!
168//! impl From<HelloWorldFdwError> for ErrorReport {
169//! fn from(_value: HelloWorldFdwError) -> Self {
170//! ErrorReport::new(PgSqlErrorCode::ERRCODE_FDW_ERROR, "", "")
171//! }
172//! }
173//!
174//! impl ForeignDataWrapper<HelloWorldFdwError> for HelloWorldFdw {
175//! fn new(server: ForeignServer) -> Result<Self, HelloWorldFdwError> {
176//! Ok(Self {
177//! row_cnt: 0,
178//! tgt_cols: Vec::new(),
179//! })
180//! }
181//!
182//! fn begin_scan(
183//! &mut self,
184//! _quals: &[Qual],
185//! columns: &[Column],
186//! _sorts: &[Sort],
187//! _limit: &Option<Limit>,
188//! _options: &HashMap<String, String>,
189//! ) -> Result<(), HelloWorldFdwError> {
190//! // reset row count
191//! self.row_cnt = 0;
192//!
193//! // save a copy of target columns
194//! self.tgt_cols = columns.to_vec();
195//! Ok(())
196//! }
197//!
198//! fn iter_scan(&mut self, row: &mut Row) -> Result<Option<()>, HelloWorldFdwError> {
199//! // this is called on each row and we only return one row here
200//! if self.row_cnt < 1 {
201//! // add values to row if they are in target column list
202//! for tgt_col in &self.tgt_cols {
203//! match tgt_col.name.as_str() {
204//! "id" => row.push("id", Some(Cell::I64(self.row_cnt))),
205//! "col" => row.push("col", Some(Cell::String("Hello world".to_string()))),
206//! _ => {}
207//! }
208//! }
209//!
210//! self.row_cnt += 1;
211//!
212//! // return the 'Some(())' to Postgres and continue data scan
213//! return Ok(Some(()));
214//! }
215//!
216//! // return 'None' to stop data scan
217//! Ok(None)
218//! }
219//!
220//! fn end_scan(&mut self) -> Result<(), HelloWorldFdwError> {
221//! // we do nothing here, but you can do things like resource cleanup and etc.
222//! Ok(())
223//! }
224//! }
225//! ```
226//!
227//! And that's it. Now your FDW is ready to run,
228//!
229//! ```bash
230//! $ cargo pgrx run
231//! ```
232//!
233//! Then create the FDW and foreign table, and make a query on it,
234//!
235//! ```sql
236//! create extension my_project;
237//!
238//! create foreign data wrapper helloworld_wrapper
239//! handler hello_world_fdw_handler
240//! validator hello_world_fdw_validator;
241//!
242//! create server my_helloworld_server
243//! foreign data wrapper helloworld_wrapper;
244//!
245//! create foreign table hello (
246//! id bigint,
247//! col text
248//! )
249//! server my_helloworld_server;
250//!
251//! select * from hello;
252//!
253//! id | col
254//! ----+-------------
255//! 0 | Hello world
256//! (1 row)
257//! ```
258//!
259//! ### Pro Tips
260//!
261//! You can use `EXPLAIN` to check what have been pushed down. For example,
262//!
263//! ```sql
264//! explain select * from hello where id = 1 order by col limit 1;
265//!
266//! QUERY PLAN
267//! --------------------------------------------------------------------------------------------------------------------------
268//! Limit (cost=1.01..1.01 rows=1 width=40)
269//! -> Sort (cost=1.01..1.01 rows=1 width=40)
270//! Sort Key: col
271//! -> Foreign Scan on hello (cost=0.00..1.00 rows=1 width=0)
272//! Filter: (id = 1)
273//! Wrappers: quals = [Qual { field: "id", operator: "=", value: Cell(I32(1)), use_or: false, param: None }]
274//! Wrappers: tgts = [Column { name: "id", num: 1, type_oid: 20 }, Column { name: "col", num: 2, type_oid: 25 }]
275//! Wrappers: sorts = [Sort { field: "col", field_no: 2, reversed: false, nulls_first: false, collate: None }]
276//! Wrappers: limit = Some(Limit { count: 1, offset: 0 })
277//! (9 rows)
278//! ```
279//!
280//! ### More FDW Examples
281//!
282//! See more FDW examples which interact with RDBMS or RESTful API.
283//! - [HelloWorld](https://github.com/supabase/wrappers/tree/main/wrappers/src/fdw/helloworld_fdw): A demo FDW to show how to develop a baisc FDW.
284//! - [BigQuery](https://github.com/supabase/wrappers/tree/main/wrappers/src/fdw/bigquery_fdw): A FDW for Google [BigQuery](https://cloud.google.com/bigquery) which supports data read and modify.
285//! - [Clickhouse](https://github.com/supabase/wrappers/tree/main/wrappers/src/fdw/clickhouse_fdw): A FDW for [ClickHouse](https://clickhouse.com/) which supports data read and modify.
286//! - [Stripe](https://github.com/supabase/wrappers/tree/main/wrappers/src/fdw/stripe_fdw): A FDW for [Stripe](https://stripe.com/) API which supports data read and modify.
287//! - [Firebase](https://github.com/supabase/wrappers/tree/main/wrappers/src/fdw/firebase_fdw): A FDW for Google [Firebase](https://firebase.google.com/) which supports data read only.
288//! - [Airtable](https://github.com/supabase/wrappers/tree/main/wrappers/src/fdw/airtable_fdw): A FDW for [Airtable](https://airtable.com/) API which supports data read only.
289//! - [S3](https://github.com/supabase/wrappers/tree/main/wrappers/src/fdw/s3_fdw): A FDW for [AWS S3](https://aws.amazon.com/s3/) which supports data read only.
290//! - [Logflare](https://github.com/supabase/wrappers/tree/main/wrappers/src/fdw/logflare_fdw): A FDW for [Logflare](https://logflare.app/) which supports data read only.
291//! - [Auth0](https://github.com/supabase/wrappers/tree/main/wrappers/src/fdw/auth0_fdw): A FDW for [Auth0](https://auth0.com/) which supports data read only.
292//! - [SQL Server](https://github.com/supabase/wrappers/tree/main/wrappers/src/fdw/mssql_fdw): A FDW for [Microsoft SQL Server](https://www.microsoft.com/en-au/sql-server/) which supports data read only.
293//! - [Redis](https://github.com/supabase/wrappers/tree/main/wrappers/src/fdw/redis_fdw): A FDW for [Redis](https://redis.io/) which supports data read only.
294
295pub mod interface;
296pub mod options;
297pub mod utils;
298
299/// The prelude includes all necessary imports to make Wrappers work
300pub mod prelude {
301 pub use crate::import_foreign_schema::*;
302 pub use crate::instance::ForeignServer;
303 pub use crate::interface::*;
304 pub use crate::options::*;
305 pub use crate::utils::*;
306 pub use crate::wrappers_fdw;
307 pub use tokio::runtime::Runtime;
308}
309
310use pgrx::prelude::*;
311use pgrx::AllocatedByPostgres;
312
313mod import_foreign_schema;
314mod instance;
315mod limit;
316mod memctx;
317mod modify;
318mod polyfill;
319mod qual;
320mod scan;
321mod sort;
322
323/// PgBox'ed `FdwRoutine`, used in [`fdw_routine`](interface::ForeignDataWrapper::fdw_routine)
324pub type FdwRoutine<A = AllocatedByPostgres> = PgBox<pg_sys::FdwRoutine, A>;
325
326pub use supabase_wrappers_macros::wrappers_fdw;