Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for jsonb data type to Airtable #181

Merged
merged 3 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ git clone https://github.com/supabase/wrappers.git

```bash
cd wrappers/wrappers
cargo pgrx run pg14 --features helloworld_fdw
cargo pgrx run pg15 --features helloworld_fdw
```

3. Create the extension, foreign data wrapper and related objects:
Expand Down Expand Up @@ -135,7 +135,7 @@ cargo pgrx test --features all_fdws,pg15
## Limitations

- Windows is not supported, that limitation inherits from [pgrx](https://github.com/tcdi/pgrx).
- Currently only supports PostgreSQL v14 and v15.
- Currently only supports PostgreSQL v14, v15 and v16.
- Generated column is not supported.

## Contribution
Expand Down
2 changes: 1 addition & 1 deletion docs/airtable.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ For example:

```sql
create foreign table my_foreign_table (
name text,
name text
-- other fields
)
server airtable_server
Expand Down
16 changes: 6 additions & 10 deletions supabase-wrappers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,15 @@ categories = ["database"]
keywords = ["database", "postgres", "postgresql", "extension"]

[features]
default = [ "cshim", "pg15" ]
cshim = [ "pgrx/cshim" ]
pg11 = ["pgrx/pg11", "pgrx-tests/pg11" ]
pg12 = ["pgrx/pg12", "pgrx-tests/pg12" ]
pg13 = ["pgrx/pg13", "pgrx-tests/pg13" ]
pg14 = ["pgrx/pg14", "pgrx-tests/pg14" ]
pg15 = ["pgrx/pg15", "pgrx-tests/pg15" ]
pg16 = ["pgrx/pg16", "pgrx-tests/pg16" ]
default = ["cshim", "pg15"]
cshim = ["pgrx/cshim"]
pg14 = ["pgrx/pg14", "pgrx-tests/pg14"]
pg15 = ["pgrx/pg15", "pgrx-tests/pg15"]
pg16 = ["pgrx/pg16", "pgrx-tests/pg16"]
pg_test = []

[dependencies]
pgrx = {version = "=0.10.2", default-features = false }
pgrx = { version = "=0.10.2", default-features = false }
thiserror = "1.0.48"
tokio = { version = "1.24", features = ["rt"] }
uuid = { version = "1.2.2" }
Expand All @@ -36,4 +33,3 @@ features = ["pg15", "cshim"]
no-default-features = true
# Enable `#[cfg(docsrs)]` (https://docs.rs/about/builds#cross-compiling)
rustc-args = ["--cfg", "docsrs"]

3 changes: 0 additions & 3 deletions wrappers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ crate-type = ["cdylib"]
[features]
default = ["cshim", "pg15"]
cshim = ["pgrx/cshim"]
pg11 = ["pgrx/pg11", "pgrx-tests/pg11", "supabase-wrappers/pg11"]
pg12 = ["pgrx/pg12", "pgrx-tests/pg12", "supabase-wrappers/pg12"]
pg13 = ["pgrx/pg13", "pgrx-tests/pg13", "supabase-wrappers/pg13"]
pg14 = ["pgrx/pg14", "pgrx-tests/pg14", "supabase-wrappers/pg14"]
pg15 = ["pgrx/pg15", "pgrx-tests/pg15", "supabase-wrappers/pg15"]
pg16 = ["pgrx/pg16", "pgrx-tests/pg16", "supabase-wrappers/pg16"]
Expand Down
24 changes: 21 additions & 3 deletions wrappers/dockerfiles/airtable/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,29 @@ def do_GET(self):

if __name__ == "__main__":
# Populate a test table
client.create(test_table, {'field1': 1, 'field2': 'two', 'field3': '2023-07-19T06:39:15.000Z'})
client.create(test_table, {'field1': 2, 'field2': 'three', 'field3': '2023-07-20T06:39:15.000Z'})
client.create(
test_table,
{
"numeric_field": 1,
"string_field": "two",
"timestamp_field": "2023-07-19T06:39:15.000Z",
"strings_array_field": ["foo", "bar"],
"object_field": {"foo": "bar"},
},
)
client.create(
test_table,
{
"numeric_field": 2,
"string_field": "three",
"timestamp_field": "2023-07-20T06:39:15.000Z",
"strings_array_field": ["baz", "qux"],
"object_field": {"foo": "baz"},
},
)

# Create a test view
airtablemock.create_view(base_id, test_table, test_view, 'field2 = "three"')
airtablemock.create_view(base_id, test_table, test_view, 'string_field = "three"')

# Create web server
webServer = HTTPServer((hostName, serverPort), AirtableMockServer)
Expand Down
1 change: 1 addition & 0 deletions wrappers/src/fdw/airtable_fdw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This is a foreign data wrapper for [Airtable](https://www.airtable.com). It is d

| Version | Date | Notes |
| ------- | ---------- | ---------------------------------------------------- |
| 0.1.3 | 2023-10-20 | Added jsonb data types support |
| 0.1.2 | 2023-07-19 | Added more data types support |
| 0.1.1 | 2023-07-13 | Added fdw stats collection |
| 0.1.0 | 2022-11-30 | Initial version |
6 changes: 3 additions & 3 deletions wrappers/src/fdw/airtable_fdw/airtable_fdw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ fn create_client(api_key: &str) -> Result<ClientWithMiddleware, AirtableFdwError
}

#[wrappers_fdw(
version = "0.1.2",
version = "0.1.3",
author = "Ankur Goyal",
website = "https://github.com/supabase/wrappers/tree/main/wrappers/src/fdw/airtable_fdw",
error_type = "AirtableFdwError"
Expand Down Expand Up @@ -100,7 +100,7 @@ impl ForeignDataWrapper<AirtableFdwError> for AirtableFdw {
Some(api_key) => Some(create_client(api_key)?),
None => {
let key_id = require_option("api_key_id", options)?;
if let Some(api_key) = get_vault_secret(&key_id) {
if let Some(api_key) = get_vault_secret(key_id) {
Some(create_client(&api_key)?)
} else {
None
Expand Down Expand Up @@ -129,7 +129,7 @@ impl ForeignDataWrapper<AirtableFdwError> for AirtableFdw {
let base_id = require_option("base_id", options)?;
let table_id = require_option("table_id", options)?;
let view_id = options.get("view_id");
let url = self.build_url(&base_id, &table_id, view_id);
let url = self.build_url(base_id, table_id, view_id);

let mut rows = Vec::new();
if let Some(client) = &self.client {
Expand Down
1 change: 1 addition & 0 deletions wrappers/src/fdw/airtable_fdw/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![allow(clippy::module_inception)]
mod airtable_fdw;
mod result;
mod tests;

use pgrx::pg_sys::panic::ErrorReport;
use pgrx::prelude::PgSqlErrorCode;
Expand Down
15 changes: 14 additions & 1 deletion wrappers/src/fdw/airtable_fdw/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ impl<'de> Deserialize<'de> for AirtableFields {
}
}

// Available Airtable field types: https://airtable.com/developers/web/api/field-model
impl AirtableRecord {
pub(super) fn to_row(&self, columns: &[Column]) -> AirtableFdwResult<Row> {
let mut row = Row::new();
Expand Down Expand Up @@ -212,7 +213,19 @@ impl AirtableRecord {
}
},
),
_ => return Err(AirtableFdwError::UnsupportedColumnType(col.name.clone())),
pg_sys::JSONBOID => self.fields.0.get(&col.name).map_or_else(
|| Ok(None),
|val| {
if val.is_array() || val.is_object() {
imor marked this conversation as resolved.
Show resolved Hide resolved
Ok(Some(Cell::Json(pgrx::JsonB(val.clone()))))
} else {
Err(())
}
},
),
_ => {
return Err(AirtableFdwError::UnsupportedColumnType(col.name.clone()));
}
}
.map_err(|_| AirtableFdwError::ColumnTypeNotMatch(col.name.clone()))?;

Expand Down
69 changes: 55 additions & 14 deletions wrappers/src/fdw/airtable_fdw/tests.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
#[cfg(any(test, feature = "pg_test"))]
#[pgrx::pg_schema]
mod tests {
use pgrx::pg_test;
use pgrx::prelude::*;
use pgrx::{prelude::*, JsonB};

#[pg_test]
fn airtable_smoketest() {
Expand All @@ -28,9 +27,11 @@ mod tests {
c.update(
r#"
CREATE FOREIGN TABLE airtable_table (
field1 numeric,
field2 text,
field3 timestamp
numeric_field numeric,
string_field text,
timestamp_field timestamp,
strings_array_field jsonb,
object_field jsonb
)
SERVER airtable_server
OPTIONS (
Expand All @@ -45,9 +46,11 @@ mod tests {
c.update(
r#"
CREATE FOREIGN TABLE airtable_view (
field1 numeric,
field2 text,
field3 timestamp
numeric_field numeric,
string_field text,
timestamp_field timestamp,
strings_array_field jsonb,
object_field jsonb
)
SERVER airtable_server
OPTIONS (
Expand All @@ -61,22 +64,60 @@ mod tests {
)
.unwrap();


/*
The table data below comes from the code in wrappers/dockerfiles/airtable/server.py
*/

let results = c
.select("SELECT field2 FROM airtable_table WHERE field = 1", None, None)
.select(
"SELECT string_field FROM airtable_table WHERE numeric_field = 1",
None,
None,
)
.unwrap()
.filter_map(|r| r.get_by_name::<&str, _>("field2").unwrap())
.filter_map(|r| r.get_by_name::<&str, _>("string_field").unwrap())
.collect::<Vec<_>>();
assert_eq!(results, vec!["two"]);

let results = c
.select("SELECT field2 FROM airtable_view", None, None)
.select(
"SELECT strings_array_field FROM airtable_table WHERE numeric_field = 1",
None,
None,
)
.unwrap()
.filter_map(|r| {
r.get_by_name::<JsonB, _>("strings_array_field")
.expect("strings_array_field is missing")
.and_then(|v| serde_json::from_value::<Vec<String>>(v.0.to_owned()).ok())
})
.collect::<Vec<_>>();

assert_eq!(results, vec![vec!["foo", "bar"]]);

#[derive(serde::Deserialize)]
struct Foo {
foo: String,
}

let results = c
.select(
"SELECT object_field FROM airtable_table WHERE numeric_field = 1",
None,
None,
)
.unwrap()
.filter_map(|r| {
r.get_by_name::<JsonB, _>("object_field")
.expect("object_field is missing")
.and_then(|v| serde_json::from_value::<Foo>(v.0.to_owned()).ok())
})
.collect::<Vec<_>>();
assert_eq!(results[0].foo, "bar");

let results = c
.select("SELECT string_field FROM airtable_view", None, None)
.unwrap()
.filter_map(|r| r.get_by_name::<&str, _>("field2").unwrap())
.filter_map(|r| r.get_by_name::<&str, _>("string_field").unwrap())
.collect::<Vec<_>>();
assert_eq!(results, vec!["three"]);
});
Expand Down
Loading