Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5492407
feat(mongodb_fdw): add cargo feature and dependencies
burmecia Jun 2, 2026
e6d34c4
ci(mongodb_fdw): add mongo:7 service to native docker-compose
burmecia Jun 2, 2026
7ce95a0
feat(mongodb_fdw): scaffold module, error type, README
burmecia Jun 2, 2026
1961baf
feat(mongodb_fdw): implement BSON to Cell type mapping
burmecia Jun 2, 2026
fd8ab31
feat(mongodb_fdw): implement Cell to BSON write-path mapping
burmecia Jun 2, 2026
25a5e29
feat(mongodb_fdw): qual/sort/limit/projection deparse helpers
burmecia Jun 2, 2026
683cc91
feat(mongodb_fdw): implement begin_scan, iter_scan, re_scan
burmecia Jun 2, 2026
a2d15e3
test(mongodb_fdw): smoketest covering count, lookup, missing field
burmecia Jun 2, 2026
3639340
test(mongodb_fdw): pushdown operators, sort, limit
burmecia Jun 2, 2026
5d7408b
test(mongodb_fdw): _doc jsonb column reads full document
burmecia Jun 2, 2026
7c80844
feat(mongodb_fdw): implement begin_modify, insert
burmecia Jun 2, 2026
88f9ccc
feat(mongodb_fdw): implement update and delete
burmecia Jun 2, 2026
dfe9a4d
test(mongodb_fdw): ObjectId hex round-trip on read and qual
burmecia Jun 2, 2026
b838ec3
docs(mongodb_fdw): add catalog page
burmecia Jun 2, 2026
0b2d0cd
chore(mongodb_fdw): cargo fmt
burmecia Jun 2, 2026
cc87387
fix(mongodb_fdw): use per-test collection names to avoid parallel-tes…
burmecia Jun 2, 2026
fe5be1f
fix(docker-compose): rename mongo service to mongodb for consistency
burmecia Jun 2, 2026
6fba4e4
test(mongodb_fdw): add comprehensive type conversion tests
burmecia Jun 3, 2026
2e848c9
fix(mongodb_fdw): enable dns-resolver for mongodb+srv URIs and surfac…
burmecia Jun 3, 2026
a8dd9b7
fix(mongodb_fdw): sanitize error messages to prevent credential leakage
burmecia Jun 3, 2026
0c47100
fix(tests): update float values in BSON tests for accuracy
burmecia Jun 3, 2026
e964cf0
feat(mongodb): add MongoDB FDW entry to the README
burmecia Jun 3, 2026
8ddae74
fix(mongodb_fdw): address PR review feedback
burmecia Jun 3, 2026
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
674 changes: 662 additions & 12 deletions Cargo.lock

Large diffs are not rendered by default.

329 changes: 329 additions & 0 deletions docs/catalog/mongodb.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
---
source:
documentation:
author: Supabase
tags:
- native
---

# MongoDB

[MongoDB](https://www.mongodb.com/) is a popular open-source document database that stores data as flexible BSON documents.

The MongoDB Wrapper allows you to read and write data from MongoDB within your Postgres database. Top-level BSON fields are mapped to declared columns by exact name; missing fields surface as SQL `NULL`. A special `_doc jsonb` column receives the full document for nested and array access.

## Preparation

Before you can query MongoDB, you need to enable the Wrappers extension and store your credentials in Postgres.

### Enable Wrappers

Make sure the `wrappers` extension is installed on your database:

```sql
create extension if not exists wrappers with schema extensions;
```

### Enable the MongoDB Wrapper

Enable the `mongodb_wrapper` FDW:

```sql
create foreign data wrapper mongodb_wrapper
handler mongodb_fdw_handler
validator mongodb_fdw_validator;
```

### Store your credentials (optional)

By default, Postgres stores FDW credentials inside `pg_catalog.pg_foreign_server` in plain text. Anyone with access to this table will be able to view these credentials. Wrappers is designed to work with [Vault](https://supabase.com/docs/guides/database/vault), which provides an additional level of security for storing credentials. We recommend using Vault to store your credentials.

```sql
-- Save your MongoDB connection string in Vault and retrieve the created `key_id`
select vault.create_secret(
'mongodb://user:password@host:27017/?replicaSet=rs0',
'mongodb',
'MongoDB connection string for Wrappers'
);
```

### Connecting to MongoDB

We need to provide Postgres with the credentials to connect to MongoDB, and any additional options. We can do this using the `create server` command:

=== "With Vault"

```sql
create server mongo_server
foreign data wrapper mongodb_wrapper
options (
conn_string_id '<key_ID>' -- The Key ID from above.
);
```

=== "Without Vault"

```sql
create server mongo_server
foreign data wrapper mongodb_wrapper
options (
conn_string 'mongodb://user:password@host:27017/?replicaSet=rs0'
);
```

The connection string follows the standard MongoDB URI format and supports `mongodb://` and `mongodb+srv://` schemes. Credentials, TLS options, replica set names, and other driver options can all be encoded in the URI.

Some connection string examples:

- `mongodb://root:secret@localhost:27017/`
- `mongodb://app_user:password@db.example.com:27017/?tls=true`
- `mongodb+srv://user:password@cluster.example.mongodb.net/?retryWrites=true`
- `mongodb://user:pass@host1:27017,host2:27017/?replicaSet=rs0`

### Create a schema

We recommend creating a schema to hold all the foreign tables:

```sql
create schema if not exists mongo;
```

## Options

The following options are available when creating MongoDB foreign tables:

- `database` - MongoDB database name, required
- `collection` - MongoDB collection name, required
- `rowid_column` - Column to use as the row identifier, optional for data scan, required for data modify. The conventional value is `_id`.

## Entities

### Collections

The MongoDB Wrapper supports data reads and writes from MongoDB collections.

#### Operations

| Object | Select | Insert | Update | Delete | Truncate |
| ----------- | :----: | :----: | :----: | :----: | :------: |
| Collections | ✅ | ✅ | ✅ | ✅ | ❌ |

#### Usage

```sql
create foreign table mongo.users (
_id text,
name text,
age int,
created_at timestamp,
_doc jsonb
)
server mongo_server
options (
database 'app',
collection 'users',
rowid_column '_id'
);
```

#### Notes

- Supports `where`, `order by`, and `limit` clause pushdown
- Documents are streamed one at a time from MongoDB; no full buffering in memory
- A column named `_doc` of type `jsonb` receives the complete document and can be used with Postgres JSON operators (`->`, `->>`) to access nested fields and arrays
- When `rowid_column` is set, INSERT, UPDATE, and DELETE are supported

## Schema Mapping

Each column declared on the foreign table maps to a top-level BSON field of the same name (exact match):

- If a document does not contain a field, the corresponding column is set to `NULL`.
- Dots in column names are treated as literal characters — they do not traverse embedded documents. Use the `_doc` column for nested field access.
- If a column named `_doc` of type `jsonb` is declared, it receives the full BSON document serialized as JSON. When `_doc` is declared, projection is disabled so that the complete document is returned from MongoDB.

Example: access a nested `address.city` field via `_doc`:

```sql
select _doc->>'name', _doc->'address'->>'city'
from mongo.users;
```

## Query Pushdown Support

This FDW supports `where`, `order by`, and `limit` clause pushdown.

### Supported Operators

The following SQL predicates are translated to MongoDB filter operators:

| SQL predicate | MongoDB filter |
| ----------------- | ----------------------- |
| `=` | `{field: {$eq: v}}` |
| `!=` | `{field: {$ne: v}}` |
| `<` | `{field: {$lt: v}}` |
| `<=` | `{field: {$lte: v}}` |
| `>` | `{field: {$gt: v}}` |
| `>=` | `{field: {$gte: v}}` |
| `IN (...)` | `{field: {$in: [...]}}` |
| `NOT IN (...)` | `{field: {$nin: [...]}}` |
| `IS NULL` | `{field: {$eq: null}}` |
| `IS NOT NULL` | `{field: {$ne: null}}` |

Multiple `where` predicates are AND'd at the top level of the filter document. Predicates with `OR` semantics are combined via `$or: [...]`. Any predicate shape that is not supported is omitted from the MongoDB filter and re-checked by Postgres after the rows are returned — the result is always correct, and the fallback is automatic.
Comment thread
burmecia marked this conversation as resolved.
Outdated

## Supported Data Types

| BSON Type | Postgres Type | Notes |
| ----------- | ----------------------- | ------------------------ |
| Boolean | bool | |
| Int32 | int2 / int4 / int8 | |
| Int64 | int8 / numeric | |
| Double | float8 / float4 | |
| Decimal128 | numeric | |
| String | text / varchar | |
| ObjectId | text | Returned as a 24-character lowercase hex string. A 24-char hex value in a qual is coerced back to `ObjectId` for the filter. |
| DateTime | timestamp / timestamptz | |
| Document | jsonb | |
| Array | jsonb | |
| Binary | bytea | |
| Null / missing | any | Column is set to `NULL` |

!!! note

`_id` fields that are `ObjectId` values are returned as their 24-character hex representation. When querying by `_id` with a 24-character hex string, the value is automatically coerced back to an `ObjectId` for the filter — no explicit casting is required.

## Writes

INSERT, UPDATE, and DELETE are enabled when `rowid_column` is set on the foreign table.

- **INSERT**: Each non-null column is written as a top-level BSON field. Null columns are omitted entirely (not stored as explicit BSON nulls). If `_id` is not supplied, MongoDB generates an `ObjectId` automatically.
- **UPDATE**: Non-null columns are passed to `$set`; null columns are passed to `$unset`, which removes the field from the document. The document is located by matching `rowid_column`.
- **DELETE**: The document matching `rowid_column = rowid` is deleted via `delete_one`.

## Limitations

This section describes important limitations and considerations when using this FDW:

- Aggregate pushdown is not supported — `COUNT`, `SUM`, `AVG`, `MIN`, and `MAX` are computed in Postgres after fetching rows
- `import foreign schema` is not supported; foreign table definitions must be declared manually
- `LIKE` predicates are not pushed down to MongoDB `$regex`; pattern matching happens in Postgres
- Nested field path access (e.g., `address.city`) is not supported as column names — use the `_doc jsonb` column and Postgres JSON operators instead
- Multi-document transactions and batched writes are not supported in v1
- Change streams are not supported
- Materialized views using foreign tables may fail during logical backups

## Examples

### Basic example

This example shows how to query a MongoDB collection from Postgres.

```sql
-- Create the server
create server mongo_server
foreign data wrapper mongodb_wrapper
options (
conn_string 'mongodb://localhost:27017/'
);

-- Create the foreign table
create foreign table mongo.users (
_id text,
name text,
age int,
created_at timestamp,
_doc jsonb
)
server mongo_server
options (
database 'app',
collection 'users'
);

-- Query all users
select _id, name, age from mongo.users;

-- Filter with pushdown
select name from mongo.users where age > 25;

-- Access nested fields via _doc
select _doc->>'email', _doc->'address'->>'city'
from mongo.users
where name = 'Alice';
```

### Data modification example

This example demonstrates INSERT, UPDATE, and DELETE on a foreign table. The `rowid_column` option is required for data modification:

```sql
create foreign table mongo.users (
_id text,
name text,
age int,
_doc jsonb
)
server mongo_server
options (
database 'app',
collection 'users',
rowid_column '_id'
);

-- Insert a new document (_id is generated by MongoDB if omitted)
insert into mongo.users (name, age)
values ('Alice', 30);

-- Update a document (null columns remove the field via $unset)
update mongo.users
set age = 31
where _id = '507f1f77bcf86cd799439011';

-- Remove a field by setting it to null
update mongo.users
set age = null
where _id = '507f1f77bcf86cd799439011';

-- Delete a document
delete from mongo.users
where _id = '507f1f77bcf86cd799439011';
```

### Using Vault for credentials

```sql
-- Store the connection string in Vault
select vault.create_secret(
'mongodb+srv://user:password@cluster.example.mongodb.net/',
'mongodb_atlas',
'MongoDB Atlas connection string'
);

-- Create the server using the Vault secret ID
create server mongo_atlas_server
foreign data wrapper mongodb_wrapper
options (
conn_string_id '<key_ID>'
);

create foreign table mongo.products (
_id text,
name text,
price numeric,
in_stock bool
)
server mongo_atlas_server
options (
database 'shop',
collection 'products',
rowid_column '_id'
);

-- Query with multiple pushed-down predicates
select name, price
from mongo.products
where in_stock = true
and price < 100
order by price
limit 10;
```
13 changes: 12 additions & 1 deletion wrappers/.ci/docker-compose-native.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -261,4 +261,15 @@ services:
test: mysqladmin ping -h localhost -P 3306 -u root -ppassword || exit 1
interval: 10s
timeout: 5s
retries: 20
retries: 20

mongodb:
image: mongo:7
container_name: mongo-local
ports:
- "27017:27017"
healthcheck:
test: ["CMD", "sh", "-c", "echo 'db.runCommand({ ping: 1 })' | mongosh --quiet"]
interval: 10s
timeout: 5s
retries: 5
15 changes: 15 additions & 0 deletions wrappers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,16 @@ mysql_fdw = [
"serde_json",
"thiserror",
]
mongodb_fdw = [
"mongodb",
"bson",
"tokio",
"futures-util",
"serde_json",
"thiserror",
"mongodb/bson-2",
"mongodb/compat-3-3-0",
]
redis_fdw = [
"redis",
"serde_json",
Expand Down Expand Up @@ -222,6 +232,7 @@ native_fdws = [
"auth0_fdw",
"mssql_fdw",
"mysql_fdw",
"mongodb_fdw",
"redis_fdw",
"cognito_fdw",
"iceberg_fdw",
Expand Down Expand Up @@ -308,6 +319,10 @@ arrow-json = { version = "57.3.0", optional = true }
# for mysql_fdw
mysql_async = { version = "0.36.1", optional = true, default-features = false, features = ["default-rustls"] }

# for mongodb_fdw
mongodb = { version = "3.2", optional = true, default-features = false, features = ["rustls-tls", "dns-resolver"] }
bson = { version = "2.13", optional = true, features = ["chrono-0_4"] }

# for mssql_fdw
tiberius = { version = "0.12.2", features = [
"tds73",
Expand Down
3 changes: 3 additions & 0 deletions wrappers/src/fdw/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ mod mssql_fdw;
#[cfg(feature = "mysql_fdw")]
mod mysql_fdw;

#[cfg(feature = "mongodb_fdw")]
mod mongodb_fdw;

#[cfg(feature = "redis_fdw")]
mod redis_fdw;

Expand Down
Loading
Loading