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;