Skip to content

Commit ed9d80e

Browse files
Add connector hardening and DuckDB write features (#713)
- offload blocking database driver operations from async workers - add generic schema projection and MongoDB declared-schema/JSON nesting support - return structured Postgres, MySQL, and SQLite conversion errors - restore DuckDB DML, write settings, cache invalidation, and file-swap overwrites - keep all DuckDB functionality on the official duckdb crate APIs Signed-off-by: Phillip LeBlanc <879445+phillipleblanc@users.noreply.github.com>
1 parent a853ff4 commit ed9d80e

33 files changed

Lines changed: 4646 additions & 261 deletions

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
//! and module paths for backward compatibility.
99
1010
pub use datafusion_table_providers_common::{
11-
common, util, Error, UnsupportedTypeAction, DESCRIPTION_METADATA_KEY,
11+
common, schema_projection, util, Error, UnsupportedTypeAction, DESCRIPTION_METADATA_KEY,
1212
};
1313

1414
pub mod sql {

core/tests/mongodb/mod.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,126 @@ async fn test_mongodb_unnesting_depth_1(port: usize) {
769769
.await;
770770
}
771771

772+
/// JSON nesting (`json_object`): declared static columns (`_id`, `name`) stay
773+
/// top-level while every other document field — scalar, nested document, and
774+
/// array — folds into one sorted-key JSON `Utf8` catch-all column (`data`).
775+
/// Exercised end-to-end through the DataFusion scan path against a live MongoDB.
776+
async fn test_mongodb_json_nesting(port: usize) {
777+
use datafusion_table_providers::schema_projection::SchemaProjection;
778+
779+
let test_docs = vec![
780+
doc! {
781+
"_id": 1,
782+
"name": "Alice",
783+
"email": "alice@example.com",
784+
"age": 30,
785+
"address": { "city": "NYC", "zip": "10001" },
786+
},
787+
doc! {
788+
"_id": 2,
789+
"name": "Bob",
790+
"email": "bob@example.com",
791+
"tags": ["x", "y"],
792+
},
793+
];
794+
795+
let ctx = SessionContext::new();
796+
let client = common::get_mongodb_client(port)
797+
.await
798+
.expect("MongoDB client should be created");
799+
let collection = client
800+
.database("testdb")
801+
.collection::<Document>("json_nesting_collection");
802+
let _ = collection.drop().await;
803+
collection
804+
.insert_many(test_docs)
805+
.await
806+
.expect("MongoDB documents should be inserted");
807+
808+
// `_id` and `name` are declared static; every other field folds into `data`.
809+
let projection = SchemaProjection::nesting(
810+
vec!["_id".to_string(), "name".to_string()],
811+
"data".to_string(),
812+
);
813+
814+
let pool = common::get_mongodb_connection_pool(port, None)
815+
.await
816+
.expect("MongoDB connection pool should be created");
817+
let table = MongoDBTable::new_with_projection(
818+
&Arc::new(pool),
819+
"json_nesting_collection",
820+
None,
821+
Some(projection),
822+
)
823+
.await
824+
.expect("Table should be created");
825+
ctx.register_table("json_nesting_collection", Arc::new(table))
826+
.expect("Table should be registered");
827+
828+
let batches = ctx
829+
.sql("SELECT name, data FROM json_nesting_collection ORDER BY _id")
830+
.await
831+
.expect("query should plan")
832+
.collect()
833+
.await
834+
.expect("query should execute");
835+
836+
let mut rows: Vec<(String, serde_json::Value)> = Vec::new();
837+
for batch in &batches {
838+
let names = batch
839+
.column_by_name("name")
840+
.expect("name column")
841+
.as_any()
842+
.downcast_ref::<StringArray>()
843+
.expect("name should be a static Utf8 column");
844+
let data = batch
845+
.column_by_name("data")
846+
.expect("catch-all data column")
847+
.as_any()
848+
.downcast_ref::<StringArray>()
849+
.expect("catch-all should be a Utf8 JSON string");
850+
for row in 0..batch.num_rows() {
851+
let catch_all: serde_json::Value =
852+
serde_json::from_str(data.value(row)).expect("catch-all must be valid JSON");
853+
rows.push((names.value(row).to_string(), catch_all));
854+
}
855+
}
856+
857+
assert_eq!(rows.len(), 2, "expected two documents");
858+
859+
// Row 0 (Alice): non-declared scalar + nested-document fields fold into the
860+
// catch-all; declared static keys must not leak into it.
861+
let (name0, data0) = &rows[0];
862+
assert_eq!(name0, "Alice");
863+
assert_eq!(data0["email"], serde_json::json!("alice@example.com"));
864+
assert!(
865+
data0.get("age").is_some(),
866+
"scalar `age` must be in the catch-all"
867+
);
868+
assert!(
869+
data0["address"].is_object(),
870+
"nested `address` must be preserved as JSON in the catch-all"
871+
);
872+
assert!(
873+
data0.get("name").is_none(),
874+
"static `name` must not leak into the catch-all"
875+
);
876+
assert!(
877+
data0.get("_id").is_none(),
878+
"static `_id` must not leak into the catch-all"
879+
);
880+
881+
// Row 1 (Bob): an array field folds in as well.
882+
let (name1, data1) = &rows[1];
883+
assert_eq!(name1, "Bob");
884+
assert_eq!(data1["email"], serde_json::json!("bob@example.com"));
885+
assert!(
886+
data1["tags"].is_array(),
887+
"array `tags` must be preserved in the catch-all"
888+
);
889+
assert!(data1.get("name").is_none());
890+
}
891+
772892
use datafusion::common::Result as DFResult;
773893
fn project_record_batch(batch: &RecordBatch, columns: &[&str]) -> DFResult<RecordBatch> {
774894
let schema = batch.schema();
@@ -812,6 +932,7 @@ async fn test_mongodb_arrow_oneway() {
812932
test_mongodb_nested_object_types(port).await;
813933
test_mongodb_null_and_missing_fields(port).await;
814934
test_mongodb_unnesting_depth_1(port).await;
935+
test_mongodb_json_nesting(port).await;
815936
test_mongodb_sort_limit(port).await;
816937

817938
mongodb_container.remove().await.expect("container to stop");

crates/adbc/src/pool.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use std::sync::Arc;
2323
use crate::conn::AdbcDbConnection;
2424
use datafusion_table_providers_common::sql::db_connection_pool::{
2525
dbconnection::{DbConnection, SyncDbConnection},
26+
runtime::run_async_with_tokio,
2627
DbConnectionPool, JoinPushDown,
2728
};
2829
type Result<T, E = Box<dyn std::error::Error + Send + Sync>> = std::result::Result<T, E>;
@@ -191,10 +192,25 @@ where
191192
) -> Result<Box<dyn DbConnection<r2d2::PooledConnection<AdbcConnectionManager<D>>, RecordBatch>>>
192193
{
193194
let pool = Arc::clone(&self.pool);
194-
let conn: r2d2::PooledConnection<AdbcConnectionManager<D>> =
195-
pool.get().context(ConnectionPoolSnafu)?;
196195

197-
Ok(Box::new(AdbcDbConnection::new(conn)))
196+
let connect = async move || -> Result<
197+
Box<dyn DbConnection<r2d2::PooledConnection<AdbcConnectionManager<D>>, RecordBatch>>,
198+
> {
199+
let conn: r2d2::PooledConnection<AdbcConnectionManager<D>> =
200+
tokio::task::spawn_blocking(move || pool.get())
201+
.await
202+
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?
203+
.context(ConnectionPoolSnafu)?;
204+
205+
Ok(Box::new(AdbcDbConnection::new(conn))
206+
as Box<
207+
dyn DbConnection<
208+
r2d2::PooledConnection<AdbcConnectionManager<D>>,
209+
RecordBatch,
210+
>,
211+
>)
212+
};
213+
run_async_with_tokio(connect).await
198214
}
199215

200216
fn join_push_down(&self) -> JoinPushDown {

crates/common/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ url = "2.5"
4949
default = ["federation"]
5050
federation = ["dep:datafusion-federation"]
5151

52+
[dev-dependencies]
53+
reqwest = "0.13"
54+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
55+
5256
[package.metadata.docs.rs]
5357
all-features = true
5458
rustdoc-args = ["--cfg", "docsrs"]

crates/common/src/common.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ impl<T, P> std::fmt::Debug for DatabaseSchemaProvider<T, P> {
5959
}
6060
}
6161

62-
impl<T, P: 'static> DatabaseSchemaProvider<T, P> {
62+
impl<T: 'static, P: 'static> DatabaseSchemaProvider<T, P> {
6363
pub async fn try_new(name: String, pool: Pool<T, P>) -> Result<Self> {
6464
let conn = pool.connect().await?;
6565
let tables = get_tables(conn, &name).await?;

crates/common/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
44
use snafu::prelude::*;
55

66
pub mod common;
7+
pub mod schema_projection;
78
pub mod sql;
89
pub mod util;
910

0 commit comments

Comments
 (0)