From 84972c842b3970a978eb36228036f0841e841d7e Mon Sep 17 00:00:00 2001 From: Norman Banick Date: Sun, 11 Jan 2026 16:24:10 +0000 Subject: [PATCH 1/2] feat: Add Oracle table provider with rust-oracle driver Implement Oracle support using rust-oracle (ODPI-C) and bb8 connection pooling. Includes comprehensive type mappings (NUMBER, DATE, TIMESTAMP, CLOB, BLOB, RAW), schema inference, and 12 integration tests. Tested against Oracle Database 23c Free. Note: This is my first contribution to the project. Feedback welcome! :) --- Cargo.lock | 109 +++- README.md | 52 ++ core/Cargo.toml | 10 + core/src/lib.rs | 2 + core/src/oracle.rs | 131 ++++ core/src/oracle/federation.rs | 104 +++ core/src/oracle/sql_table.rs | 178 ++++++ core/src/oracle/write.rs | 21 + core/src/sql/arrow_sql_gen/mod.rs | 2 + core/src/sql/arrow_sql_gen/oracle.rs | 278 ++++++++ .../sql/db_connection_pool/dbconnection.rs | 2 + .../dbconnection/oracleconn.rs | 286 +++++++++ core/src/sql/db_connection_pool/mod.rs | 2 + core/src/sql/db_connection_pool/oraclepool.rs | 150 +++++ core/tests/integration.rs | 2 + core/tests/oracle/common.rs | 106 ++++ core/tests/oracle/mod.rs | 593 ++++++++++++++++++ instantclient-basiclite-linuxx64.zip | Bin 0 -> 11350209 bytes 18 files changed, 2018 insertions(+), 10 deletions(-) create mode 100644 core/src/oracle.rs create mode 100644 core/src/oracle/federation.rs create mode 100644 core/src/oracle/sql_table.rs create mode 100644 core/src/oracle/write.rs create mode 100644 core/src/sql/arrow_sql_gen/oracle.rs create mode 100644 core/src/sql/db_connection_pool/dbconnection/oracleconn.rs create mode 100644 core/src/sql/db_connection_pool/oraclepool.rs create mode 100644 core/tests/oracle/common.rs create mode 100644 core/tests/oracle/mod.rs create mode 100644 instantclient-basiclite-linuxx64.zip diff --git a/Cargo.lock b/Cargo.lock index e093d349..46706ca8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -690,6 +690,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "bb8-oracle" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a471c2e1027f28a98972d3d38bd2612efd05cf7b27d6cc1566ec901537e2d1c8" +dependencies = [ + "bb8", + "oracle", + "tokio", +] + [[package]] name = "bb8-postgres" version = "0.9.0" @@ -986,9 +997,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.41" +version = "1.2.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" dependencies = [ "find-msvc-tools", "jobserver", @@ -1314,14 +1325,38 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + [[package]] name = "darling" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.109", ] [[package]] @@ -1334,17 +1369,28 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.11.1", "syn 2.0.111", ] +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core 0.13.4", + "quote", + "syn 1.0.109", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", "quote", "syn 2.0.111", ] @@ -2111,6 +2157,7 @@ dependencies = [ "async-trait", "base64", "bb8", + "bb8-oracle", "bb8-postgres", "bigdecimal", "bollard", @@ -2139,6 +2186,7 @@ dependencies = [ "native-tls", "num-bigint", "odbc-api", + "oracle", "pem", "postgres-native-tls", "prost", @@ -2353,9 +2401,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" [[package]] name = "fixedbitset" @@ -3561,7 +3609,7 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66f62cad7623a9cb6f8f64037f0c4f69c8db8e82914334a83c9788201c2c1bfa" dependencies = [ - "darling", + "darling 0.20.11", "heck 0.5.0", "num-bigint", "proc-macro-crate", @@ -4038,6 +4086,15 @@ version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd7e3c4b5b7bbd3e7bd01dc00cb4614f2445591cad1f6f18a7e16d7f98c392e9" +[[package]] +name = "odpic-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920b5474a5128a9f0232df5a0ffc50aaa5b077b29b8b06ab0131985ac82793ed" +dependencies = [ + "cc", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -4094,6 +4151,32 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "oracle" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db40fe6e4df881b683691ade5ef1f7b1afd52aefa115581f7b92855524d7ec0" +dependencies = [ + "cc", + "odpic-sys", + "once_cell", + "oracle_procmacro", + "paste", + "rustversion", +] + +[[package]] +name = "oracle_procmacro" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad247f3421d57de56a0d0408d3249d4b1048a522be2013656d92f022c3d8af27" +dependencies = [ + "darling 0.13.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "orbclient" version = "0.3.48" @@ -5089,7 +5172,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bae0cbad6ab996955664982739354128c58d16e126114fe88c2a493642502aab" dependencies = [ - "darling", + "darling 0.20.11", "heck 0.4.1", "proc-macro2", "quote", @@ -5482,6 +5565,12 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strsim" version = "0.11.1" diff --git a/README.md b/README.md index 2976d271..55cab613 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ let ctx = SessionContext::with_state(state); - PostgreSQL - MySQL +- Oracle - SQLite - ClickHouse - DuckDB @@ -165,6 +166,57 @@ EOF cargo run -p datafusion-table-providers --example mysql --features mysql ``` +### Oracle + +In order to run the Oracle example, you need to have an Oracle database server running. You can use the following command to start an Oracle Free server in a Docker container the example can use: + +```bash +docker run --name oracle-free \ + -e ORACLE_PASSWORD=OraclePassword123 \ + -p 1521:1521 \ + -d gvenzl/oracle-free:latest + +# Wait for the Oracle server to start and healthcheck to pass +echo "Waiting for Oracle to start (this may take 1-2 minutes)..." +until docker exec oracle-free /usr/local/bin/checkHealth.sh >/dev/null 2>&1; do + sleep 5 +done +echo "Oracle is ready!" + +# Create a table in the Oracle server and insert some data +docker exec -i oracle-free sqlplus system/OraclePassword123@FREEPDB1 <, + }, +} + +pub type Result = std::result::Result; + +pub struct OracleTableFactory { + pool: Arc, +} + +impl OracleTableFactory { + #[must_use] + pub fn new(pool: Arc) -> Self { + Self { pool } + } + + pub async fn table_provider( + &self, + table_reference: TableReference, + ) -> Result, Box> { + let pool = Arc::clone(&self.pool); + let dyn_pool = pool as Arc< + dyn db_connection_pool::DbConnectionPool< + OraclePooledConnection, + oracle::sql_type::OracleType, + > + Send + + Sync + + 'static, + >; + + let table = SqlTable::new("oracle", &dyn_pool, table_reference) + .await + .map_err(|e| Box::new(e) as Box)?; + + let oracle_table = Arc::new(OracleTable::new(Arc::clone(&self.pool), table)); + + #[cfg(feature = "oracle-federation")] + let oracle_table = Arc::new( + oracle_table + .create_federated_table_provider() + .map_err(|e| Box::new(e) as Box)?, + ); + + Ok(oracle_table) + } +} + +#[derive(Debug)] +pub struct OracleTableProviderFactory {} + +impl OracleTableProviderFactory { + #[must_use] + pub fn new() -> Self { + Self {} + } +} + +impl Default for OracleTableProviderFactory { + fn default() -> Self { + Self::new() + } +} + +#[async_trait] +impl TableProviderFactory for OracleTableProviderFactory { + async fn create( + &self, + _state: &dyn Session, + cmd: &CreateExternalTable, + ) -> datafusion::common::Result> { + let name = cmd.name.to_string(); + let options = &cmd.options; + + // Construct params from options + let mut params: HashMap = HashMap::new(); + for (k, v) in options { + params.insert(k.clone(), SecretString::from(v.clone())); + } + + let pool = OracleConnectionPool::new(params) + .await + .map_err(|e| DataFusionError::External(Box::new(e)))?; + + let factory = OracleTableFactory::new(Arc::new(pool)); + + let table = factory + .table_provider(TableReference::from(name)) + .await + .map_err(DataFusionError::External)?; + + Ok(table) + } +} diff --git a/core/src/oracle/federation.rs b/core/src/oracle/federation.rs new file mode 100644 index 00000000..d1047f5e --- /dev/null +++ b/core/src/oracle/federation.rs @@ -0,0 +1,104 @@ +use crate::sql::db_connection_pool::dbconnection::oracleconn::OraclePooledConnection; +use crate::sql::db_connection_pool::dbconnection::{get_schema, Error as DbError}; +use crate::sql::sql_provider_datafusion::{get_stream, to_execution_error}; +use arrow::datatypes::SchemaRef; +use async_trait::async_trait; +use datafusion::sql::unparser::dialect::Dialect; +use datafusion_federation::sql::{ + RemoteTableRef, SQLExecutor, SQLFederationProvider, SQLTableSource, +}; +use datafusion_federation::{FederatedTableProviderAdaptor, FederatedTableSource}; +use futures::TryStreamExt; +use snafu::ResultExt; +use std::sync::Arc; + +use super::sql_table::OracleTable; +use datafusion::{ + datasource::TableProvider, + error::{DataFusionError, Result as DataFusionResult}, + execution::SendableRecordBatchStream, + physical_plan::stream::RecordBatchStreamAdapter, + sql::TableReference, +}; + +impl OracleTable { + pub fn create_federated_table_source( + self: Arc, + ) -> DataFusionResult> { + let table_reference = self.base_table.table_reference.clone(); + let schema = Arc::clone(&self.base_table.schema()); + let fed_provider = Arc::new(SQLFederationProvider::new(self.clone())); + Ok(Arc::new(SQLTableSource::new_with_schema( + fed_provider, + RemoteTableRef::from(table_reference), + schema, + ))) + } + + pub fn create_federated_table_provider( + self: Arc, + ) -> DataFusionResult { + let table_source = self.clone().create_federated_table_source()?; + Ok(FederatedTableProviderAdaptor::new_with_provider( + table_source, + self, + )) + } +} + +#[async_trait] +impl SQLExecutor for OracleTable { + fn name(&self) -> &str { + self.base_table.name() + } + + fn compute_context(&self) -> Option { + None + } + + fn dialect(&self) -> Arc { + Arc::new(datafusion::sql::unparser::dialect::PostgreSqlDialect {}) + } + + fn execute( + &self, + query: &str, + schema: SchemaRef, + ) -> DataFusionResult { + let pool = self.base_table.clone_pool(); + let dyn_pool = pool as Arc< + dyn crate::sql::db_connection_pool::DbConnectionPool< + OraclePooledConnection, + oracle::sql_type::OracleType, + > + Send + + Sync, + >; + let fut = get_stream(dyn_pool, query.to_string(), Arc::clone(&schema)); + + let stream = futures::stream::once(fut).try_flatten(); + Ok(Box::pin(RecordBatchStreamAdapter::new(schema, stream))) + } + + async fn table_names(&self) -> DataFusionResult> { + Err(DataFusionError::NotImplemented( + "table inference not implemented".to_string(), + )) + } + + async fn get_table_schema(&self, table_name: &str) -> DataFusionResult { + let pool = self.base_table.clone_pool(); + let dyn_pool = pool as Arc< + dyn crate::sql::db_connection_pool::DbConnectionPool< + OraclePooledConnection, + oracle::sql_type::OracleType, + > + Send + + Sync, + >; + let conn = dyn_pool.connect().await.map_err(to_execution_error)?; + get_schema(conn, &TableReference::from(table_name)) + .await + .boxed() + .map_err(|e| DbError::UnableToGetSchema { source: e }) + .map_err(to_execution_error) + } +} diff --git a/core/src/oracle/sql_table.rs b/core/src/oracle/sql_table.rs new file mode 100644 index 00000000..ce268505 --- /dev/null +++ b/core/src/oracle/sql_table.rs @@ -0,0 +1,178 @@ +use crate::sql::db_connection_pool::oraclepool::OracleConnectionPool; + +use async_trait::async_trait; +use datafusion::catalog::Session; +use futures::TryStreamExt; +use std::fmt::Display; +use std::{any::Any, fmt, sync::Arc}; + +use crate::sql::db_connection_pool::dbconnection::oracleconn::OraclePooledConnection; +use crate::sql::sql_provider_datafusion::{ + get_stream, to_execution_error, Result as SqlResult, SqlExec, SqlTable, +}; +use datafusion::{ + arrow::datatypes::SchemaRef, + datasource::TableProvider, + error::Result as DataFusionResult, + execution::TaskContext, + logical_expr::{Expr, TableProviderFilterPushDown, TableType}, + physical_plan::{ + stream::RecordBatchStreamAdapter, DisplayAs, DisplayFormatType, ExecutionPlan, + PlanProperties, SendableRecordBatchStream, + }, +}; + +pub struct OracleTable { + pool: Arc, + pub(crate) base_table: SqlTable, +} + +impl std::fmt::Debug for OracleTable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("OracleTable") + .field("base_table", &self.base_table) + .finish() + } +} + +impl OracleTable { + pub fn new( + pool: Arc, + base_table: SqlTable, + ) -> Self { + Self { pool, base_table } + } + + fn create_physical_plan( + &self, + projections: Option<&Vec>, + schema: &SchemaRef, + filters: &[Expr], + limit: Option, + ) -> DataFusionResult> { + let sql = self.base_table.scan_to_sql(projections, filters, limit)?; + Ok(Arc::new(OracleSQLExec::new( + projections, + schema, + Arc::clone(&self.pool), + sql, + )?)) + } +} + +#[async_trait] +impl TableProvider for OracleTable { + fn as_any(&self) -> &dyn Any { + self + } + + fn schema(&self) -> SchemaRef { + self.base_table.schema() + } + + fn table_type(&self) -> TableType { + self.base_table.table_type() + } + + fn supports_filters_pushdown( + &self, + filters: &[&Expr], + ) -> DataFusionResult> { + self.base_table.supports_filters_pushdown(filters) + } + + async fn scan( + &self, + _state: &dyn Session, + projection: Option<&Vec>, + filters: &[Expr], + limit: Option, + ) -> DataFusionResult> { + return self.create_physical_plan(projection, &self.schema(), filters, limit); + } +} + +impl Display for OracleTable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "OracleTable {}", self.base_table.name()) + } +} + +struct OracleSQLExec { + base_exec: SqlExec, +} + +impl OracleSQLExec { + fn new( + projections: Option<&Vec>, + schema: &SchemaRef, + pool: Arc, + sql: String, + ) -> DataFusionResult { + let base_exec = SqlExec::new(projections, schema, pool, sql)?; + + Ok(Self { base_exec }) + } + + fn sql(&self) -> SqlResult { + self.base_exec.sql() + } +} + +impl std::fmt::Debug for OracleSQLExec { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let sql = self.sql().unwrap_or_default(); + write!(f, "OracleSQLExec sql={sql}") + } +} + +impl DisplayAs for OracleSQLExec { + fn fmt_as(&self, _t: DisplayFormatType, f: &mut fmt::Formatter) -> std::fmt::Result { + let sql = self.sql().unwrap_or_default(); + write!(f, "OracleSQLExec sql={sql}") + } +} + +impl ExecutionPlan for OracleSQLExec { + fn name(&self) -> &'static str { + "OracleSQLExec" + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn schema(&self) -> SchemaRef { + self.base_exec.schema() + } + + fn properties(&self) -> &PlanProperties { + self.base_exec.properties() + } + + fn children(&self) -> Vec<&Arc> { + self.base_exec.children() + } + + fn with_new_children( + self: Arc, + _children: Vec>, + ) -> DataFusionResult> { + Ok(self) + } + + fn execute( + &self, + _partition: usize, + _context: Arc, + ) -> DataFusionResult { + let sql = self.sql().map_err(to_execution_error)?; + tracing::debug!("OracleSQLExec sql: {sql}"); + + let fut = get_stream(self.base_exec.clone_pool(), sql, Arc::clone(&self.schema())); + + let stream = futures::stream::once(fut).try_flatten(); + let schema = Arc::clone(&self.schema()); + Ok(Box::pin(RecordBatchStreamAdapter::new(schema, stream))) + } +} diff --git a/core/src/oracle/write.rs b/core/src/oracle/write.rs new file mode 100644 index 00000000..8c321b83 --- /dev/null +++ b/core/src/oracle/write.rs @@ -0,0 +1,21 @@ +use crate::sql::db_connection_pool::oraclepool::OracleConnectionPool; +use datafusion::arrow::record_batch::RecordBatch; +use datafusion::error::{DataFusionError, Result}; +use std::sync::Arc; + +pub struct OracleTableWriter { + _pool: Arc, +} + +impl OracleTableWriter { + pub fn new(pool: Arc) -> Self { + Self { _pool: pool } + } + + pub async fn insert_batch(&self, _batch: RecordBatch) -> Result { + // Implement batch insert using oracle-rs BatchBuilder + Err(DataFusionError::NotImplemented( + "Oracle write support not yet implemented".to_string(), + )) + } +} diff --git a/core/src/sql/arrow_sql_gen/mod.rs b/core/src/sql/arrow_sql_gen/mod.rs index 606b831a..d0e8947b 100644 --- a/core/src/sql/arrow_sql_gen/mod.rs +++ b/core/src/sql/arrow_sql_gen/mod.rs @@ -45,6 +45,8 @@ pub mod arrow; #[cfg(feature = "mysql")] pub mod mysql; +#[cfg(feature = "oracle")] +pub mod oracle; #[cfg(feature = "postgres")] pub mod postgres; #[cfg(feature = "sqlite")] diff --git a/core/src/sql/arrow_sql_gen/oracle.rs b/core/src/sql/arrow_sql_gen/oracle.rs new file mode 100644 index 00000000..bb965fe7 --- /dev/null +++ b/core/src/sql/arrow_sql_gen/oracle.rs @@ -0,0 +1,278 @@ +use crate::sql::arrow_sql_gen::arrow::map_data_type_to_array_builder_optional; +use arrow::{ + array::{ + ArrayBuilder, ArrayRef, BinaryBuilder, Decimal128Builder, Decimal256Builder, + LargeBinaryBuilder, LargeStringBuilder, StringBuilder, TimestampMicrosecondBuilder, + }, + datatypes::{i256, DataType, Field, Schema, SchemaRef, TimeUnit}, + error::ArrowError, + record_batch::RecordBatch, +}; + +use bigdecimal::num_bigint; +use bigdecimal::{BigDecimal, ToPrimitive}; +use chrono::{TimeZone, Utc}; +use oracle::Row; +use snafu::{ResultExt, Snafu}; +use std::sync::Arc; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("Failed to build record batch: {source}"))] + FailedToBuildRecordBatch { source: ArrowError }, + + #[snafu(display("No builder found for index {index}"))] + NoBuilderForIndex { index: usize }, + + #[snafu(display("Failed to downcast builder for index {index}"))] + FailedToDowncastBuilder { index: usize }, + + #[snafu(display("Oracle error: {source}"))] + OracleError { source: oracle::Error }, + + #[snafu(display("Cannot represent BigDecimal as i128: {big_decimal}"))] + FailedToConvertBigDecimalToI128 { big_decimal: BigDecimal }, + + #[snafu(display("Failed to parse BigDecimal from string '{value}': {source}"))] + ParseBigDecimalError { + value: String, + source: bigdecimal::ParseBigDecimalError, + }, +} + +pub type Result = std::result::Result; + +pub fn rows_to_arrow(rows: Vec, projected_schema: &Option) -> Result { + if rows.is_empty() { + return Ok(RecordBatch::new_empty( + projected_schema + .clone() + .unwrap_or_else(|| Arc::new(Schema::empty())), + )); + } + + let mut builders: Vec> = Vec::new(); + let mut arrow_fields: Vec = Vec::new(); + + let first_row = &rows[0]; + + // Determine schema fields + if let Some(schema) = projected_schema { + for field in schema.fields() { + arrow_fields.push((**field).clone()); + builders + .push(map_data_type_to_array_builder_optional(Some(field.data_type())).unwrap()); + } + } else { + // Infer from first row - using ODPI-C metadata if available + // In rust-oracle, Row doesn't directly expose column names easily without Statement metadata. + // However, we usually have a projected_schema from get_schema which queries metadata. + + let column_count = first_row.column_info().len(); + for i in 0..column_count { + let name = format!("col_{}", i); + let data_type = DataType::Utf8; + let field = Field::new(name, data_type.clone(), true); + arrow_fields.push(field); + builders.push(map_data_type_to_array_builder_optional(Some(&data_type)).unwrap()); + } + } + + for row in rows { + for (i, builder) in builders.iter_mut().enumerate() { + let field = &arrow_fields[i]; + + match field.data_type() { + DataType::Utf8 => { + let builder = builder + .as_any_mut() + .downcast_mut::() + .ok_or(Error::FailedToDowncastBuilder { index: i })?; + let val: Option = row + .get::<_, Option>(i) + .map_err(|e| Error::OracleError { source: e })?; + builder.append_option(val); + } + DataType::Float64 => { + use arrow::array::Float64Builder; + let builder = builder + .as_any_mut() + .downcast_mut::() + .ok_or(Error::FailedToDowncastBuilder { index: i })?; + let val: Option = row + .get::<_, Option>(i) + .map_err(|e| Error::OracleError { source: e })?; + builder.append_option(val); + } + DataType::Float32 => { + use arrow::array::Float32Builder; + let builder = builder + .as_any_mut() + .downcast_mut::() + .ok_or(Error::FailedToDowncastBuilder { index: i })?; + let val: Option = row + .get::<_, Option>(i) + .map_err(|e| Error::OracleError { source: e })?; + builder.append_option(val); + } + DataType::Int64 => { + use arrow::array::Int64Builder; + let builder = builder + .as_any_mut() + .downcast_mut::() + .ok_or(Error::FailedToDowncastBuilder { index: i })?; + let val: Option = row + .get::<_, Option>(i) + .map_err(|e| Error::OracleError { source: e })?; + builder.append_option(val); + } + DataType::Decimal128(_p, s) => { + let builder = builder + .as_any_mut() + .downcast_mut::() + .ok_or(Error::FailedToDowncastBuilder { index: i })?; + let val: Option = row + .get::<_, Option>(i) + .map_err(|e| Error::OracleError { source: e })?; + if let Some(s_val) = val { + let big_dec = s_val.parse::().map_err(|e| { + Error::ParseBigDecimalError { + value: s_val.clone(), + source: e, + } + })?; + let i128_val = to_decimal_128(&big_dec, *s as i64).ok_or( + Error::FailedToConvertBigDecimalToI128 { + big_decimal: big_dec, + }, + )?; + builder.append_value(i128_val); + } else { + builder.append_null(); + } + } + DataType::Decimal256(_p, _s) => { + let builder = builder + .as_any_mut() + .downcast_mut::() + .ok_or(Error::FailedToDowncastBuilder { index: i })?; + let val: Option = row + .get::<_, Option>(i) + .map_err(|e| Error::OracleError { source: e })?; + if let Some(s_val) = val { + let big_dec = s_val.parse::().map_err(|e| { + Error::ParseBigDecimalError { + value: s_val.clone(), + source: e, + } + })?; + let i256_val = to_decimal_256(&big_dec); + builder.append_value(i256_val); + } else { + builder.append_null(); + } + } + DataType::Timestamp(TimeUnit::Microsecond, _) => { + let builder = builder + .as_any_mut() + .downcast_mut::() + .ok_or(Error::FailedToDowncastBuilder { index: i })?; + let val: Option = row + .get::<_, Option>(i) + .map_err(|e| Error::OracleError { source: e })?; + if let Some(ts) = val { + let chrono_ts = Utc + .with_ymd_and_hms( + ts.year(), + ts.month(), + ts.day(), + ts.hour(), + ts.minute(), + ts.second(), + ) + .single(); + + if let Some(chrono_ts) = chrono_ts { + let micros = + chrono_ts.timestamp() * 1_000_000 + (ts.nanosecond() / 1000) as i64; + builder.append_value(micros); + } else { + builder.append_null(); + } + } else { + builder.append_null(); + } + } + DataType::Binary => { + let builder = builder + .as_any_mut() + .downcast_mut::() + .ok_or(Error::FailedToDowncastBuilder { index: i })?; + let val: Option> = row + .get::<_, Option>>(i) + .map_err(|e| Error::OracleError { source: e })?; + builder.append_option(val); + } + DataType::LargeBinary => { + let builder = builder + .as_any_mut() + .downcast_mut::() + .ok_or(Error::FailedToDowncastBuilder { index: i })?; + let val: Option> = row + .get::<_, Option>>(i) + .map_err(|e| Error::OracleError { source: e })?; + builder.append_option(val); + } + DataType::LargeUtf8 => { + let builder = builder + .as_any_mut() + .downcast_mut::() + .ok_or(Error::FailedToDowncastBuilder { index: i })?; + let val: Option = row + .get::<_, Option>(i) + .map_err(|e| Error::OracleError { source: e })?; + builder.append_option(val); + } + _ => { + // Fallback: try to get as string + let builder = builder + .as_any_mut() + .downcast_mut::() + .ok_or(Error::FailedToDowncastBuilder { index: i })?; + let val: Option = row + .get::<_, Option>(i) + .map_err(|e| Error::OracleError { source: e })?; + builder.append_option(val); + } + } + } + } + + let arrays: Vec = builders.into_iter().map(|mut b| b.finish()).collect(); + let schema = Arc::new(Schema::new(arrow_fields)); + + RecordBatch::try_new(schema, arrays).context(FailedToBuildRecordBatchSnafu) +} + +fn to_decimal_128(decimal: &BigDecimal, scale: i64) -> Option { + (decimal * 10i128.pow(scale.try_into().unwrap_or_default())).to_i128() +} + +fn to_decimal_256(decimal: &BigDecimal) -> i256 { + let (bigint_value, _) = decimal.as_bigint_and_exponent(); + let mut bigint_bytes = bigint_value.to_signed_bytes_le(); + + let is_negative = bigint_value.sign() == num_bigint::Sign::Minus; + let fill_byte = if is_negative { 0xFF } else { 0x00 }; + + if bigint_bytes.len() > 32 { + bigint_bytes.truncate(32); + } else { + bigint_bytes.resize(32, fill_byte); + }; + + let mut array = [0u8; 32]; + array.copy_from_slice(&bigint_bytes); + + i256::from_le_bytes(array) +} diff --git a/core/src/sql/db_connection_pool/dbconnection.rs b/core/src/sql/db_connection_pool/dbconnection.rs index 94590026..8eb6164a 100644 --- a/core/src/sql/db_connection_pool/dbconnection.rs +++ b/core/src/sql/db_connection_pool/dbconnection.rs @@ -13,6 +13,8 @@ pub mod duckdbconn; pub mod mysqlconn; #[cfg(feature = "odbc")] pub mod odbcconn; +#[cfg(feature = "oracle")] +pub mod oracleconn; #[cfg(feature = "postgres")] pub mod postgresconn; #[cfg(feature = "sqlite")] diff --git a/core/src/sql/db_connection_pool/dbconnection/oracleconn.rs b/core/src/sql/db_connection_pool/dbconnection/oracleconn.rs new file mode 100644 index 00000000..33bed116 --- /dev/null +++ b/core/src/sql/db_connection_pool/dbconnection/oracleconn.rs @@ -0,0 +1,286 @@ +use async_trait::async_trait; +use bb8_oracle::OracleConnectionManager; +use datafusion::{ + arrow::datatypes::SchemaRef, execution::SendableRecordBatchStream, sql::TableReference, +}; +use std::{any::Any, sync::Arc}; + +use snafu::ResultExt; +use tokio::task; + +use crate::sql::{ + arrow_sql_gen::oracle::rows_to_arrow, + db_connection_pool::dbconnection::{ + AsyncDbConnection, DbConnection, Error, GenericError, Result, + }, +}; + +pub type OraclePooledConnection = bb8::PooledConnection<'static, OracleConnectionManager>; + +pub struct OracleConnection { + pub conn: OraclePooledConnection, +} + +impl OracleConnection { + pub fn new(conn: OraclePooledConnection) -> Self { + Self { conn } + } +} + +impl DbConnection for OracleConnection { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_async( + &self, + ) -> Option<&dyn AsyncDbConnection> { + Some(self) + } +} + +#[async_trait] +impl AsyncDbConnection for OracleConnection { + fn new(conn: OraclePooledConnection) -> Self { + Self { conn } + } + + async fn get_schema( + &self, + table_reference: &TableReference, + ) -> std::result::Result { + let table_name = table_reference.table().to_uppercase(); + let schema_name = table_reference.schema().map(|s| s.to_uppercase()); + + let conn = self.conn.clone(); + + let rows = task::spawn_blocking(move || { + if let Some(schema) = schema_name { + let rows = conn.query( + "SELECT column_name, data_type, data_precision, data_scale, nullable + FROM all_tab_columns + WHERE owner = :1 AND table_name = :2 + ORDER BY column_id", + &[&schema, &table_name], + )?; + rows.collect::, _>>() + } else { + let rows = conn.query( + "SELECT column_name, data_type, data_precision, data_scale, nullable + FROM all_tab_columns + WHERE table_name = :1 + ORDER BY column_id", + &[&table_name], + )?; + rows.collect::, _>>() + } + }) + .await + .map_err(|e| Box::new(e) as GenericError) + .context(super::UnableToGetSchemaSnafu)? + .map_err(|e| Box::new(e) as GenericError) + .context(super::UnableToGetSchemaSnafu)?; + + let mut fields: Vec = Vec::new(); + + for row in rows { + let column_name: String = row + .get(0) + .map_err(|e| Box::new(e) as GenericError) + .context(super::UnableToGetSchemaSnafu)?; + let data_type_str: String = row + .get(1) + .map_err(|e| Box::new(e) as GenericError) + .context(super::UnableToGetSchemaSnafu)?; + let precision: Option = row + .get(2) + .map_err(|e| Box::new(e) as GenericError) + .context(super::UnableToGetSchemaSnafu)?; + let scale: Option = row + .get(3) + .map_err(|e| Box::new(e) as GenericError) + .context(super::UnableToGetSchemaSnafu)?; + let nullable_str: String = row + .get(4) + .map_err(|e| Box::new(e) as GenericError) + .context(super::UnableToGetSchemaSnafu)?; + let nullable = nullable_str != "N"; + + let arrow_type = map_oracle_type_to_arrow(&data_type_str, precision, scale); + + fields.push(datafusion::arrow::datatypes::Field::new( + column_name, // Keep original case from Oracle + arrow_type, + nullable, + )); + } + + Ok(Arc::new(datafusion::arrow::datatypes::Schema::new(fields))) + } + + async fn query_arrow( + &self, + sql: &str, + _params: &[oracle::sql_type::OracleType], + projected_schema: Option, + ) -> Result { + let sql = sql.to_string(); + let conn = self.conn.clone(); + + let rows = task::spawn_blocking(move || { + let rows = conn.query(&sql, &[])?; + rows.collect::, _>>() + }) + .await + .map_err(|e| Box::new(e) as GenericError) + .context(super::UnableToQueryArrowSnafu)? + .map_err(|e| Box::new(e) as GenericError) + .context(super::UnableToQueryArrowSnafu)?; + + let batch = rows_to_arrow(rows, &projected_schema) + .map_err(|e| Box::new(e) as GenericError) + .context(super::UnableToQueryArrowSnafu) + .map_err(|e| Box::new(e) as GenericError)?; + + let schema = batch.schema(); + let stream = futures::stream::iter(vec![Ok(batch)]); + Ok(Box::pin( + datafusion::physical_plan::stream::RecordBatchStreamAdapter::new( + projected_schema.unwrap_or(schema), + stream, + ), + )) + } + + async fn execute(&self, sql: &str, _params: &[oracle::sql_type::OracleType]) -> Result { + let sql = sql.to_string(); + let conn = self.conn.clone(); + + let row_count = task::spawn_blocking(move || { + let stmt = conn.execute(&sql, &[])?; + stmt.row_count() + }) + .await + .map_err(|e| Box::new(e) as GenericError) + .context(super::UnableToQueryArrowSnafu)? + .map_err(|e| Box::new(e) as GenericError) + .context(super::UnableToQueryArrowSnafu)?; + + Ok(row_count) + } + + async fn tables(&self, schema: &str) -> std::result::Result, Error> { + let schema = schema.to_uppercase(); + let conn = self.conn.clone(); + + let table_names = task::spawn_blocking(move || { + let rows = conn.query( + "SELECT table_name FROM all_tables WHERE owner = :1", + &[&schema], + )?; + let mut result = Vec::new(); + for row in rows { + let row = row?; + let val: String = row.get(0)?; + result.push(val); + } + Ok::, oracle::Error>(result) + }) + .await + .map_err(|e| Box::new(e) as GenericError) + .context(super::UnableToGetTablesSnafu)? + .map_err(|e| Box::new(e) as GenericError) + .context(super::UnableToGetTablesSnafu)?; + + Ok(table_names) + } + + async fn schemas(&self) -> std::result::Result, Error> { + let conn = self.conn.clone(); + + let schemas = task::spawn_blocking(move || { + let rows = conn.query("SELECT username FROM all_users", &[])?; + let mut result = Vec::new(); + for row in rows { + let row = row?; + let val: String = row.get(0)?; + result.push(val); + } + Ok::, oracle::Error>(result) + }) + .await + .map_err(|e| Box::new(e) as GenericError) + .context(super::UnableToGetSchemasSnafu)? + .map_err(|e| Box::new(e) as GenericError) + .context(super::UnableToGetSchemasSnafu)?; + + Ok(schemas) + } +} + +/// Map Oracle data types to Arrow data types +fn map_oracle_type_to_arrow( + oracle_type: &str, + precision: Option, + scale: Option, +) -> datafusion::arrow::datatypes::DataType { + use datafusion::arrow::datatypes::DataType; + + let type_upper = oracle_type.to_uppercase(); + + // Handle types with parameters like VARCHAR2(100) + let base_type = if let Some(paren_pos) = type_upper.find('(') { + &type_upper[..paren_pos] + } else { + &type_upper + }; + + match base_type.trim() { + // String types + "VARCHAR2" | "NVARCHAR2" | "CHAR" | "NCHAR" => DataType::Utf8, + "CLOB" | "NCLOB" | "LONG" => DataType::LargeUtf8, + + // Numeric types + "NUMBER" | "NUMERIC" | "DECIMAL" | "DEC" => { + let p = precision.unwrap_or(38) as u8; + let s = scale.unwrap_or(0) as i8; + if p > 38 { + DataType::Decimal256(p, s) + } else { + DataType::Decimal128(p, s) + } + } + "INTEGER" | "INT" | "SMALLINT" => DataType::Int64, + "FLOAT" | "REAL" | "DOUBLE PRECISION" => DataType::Float64, + "BINARY_FLOAT" => DataType::Float32, + "BINARY_DOUBLE" => DataType::Float64, + + // Date/Time types + "DATE" => { + use datafusion::arrow::datatypes::TimeUnit; + DataType::Timestamp(TimeUnit::Microsecond, None) + } + _ if type_upper.contains("TIMESTAMP") => { + use datafusion::arrow::datatypes::TimeUnit; + let tz = if type_upper.contains("WITH TIME ZONE") + || type_upper.contains("WITH LOCAL TIME ZONE") + { + Some("UTC".into()) + } else { + None + }; + DataType::Timestamp(TimeUnit::Microsecond, tz) + } + + // Binary types + "RAW" => DataType::Binary, + "BLOB" | "LONG RAW" => DataType::LargeBinary, + + // Other types - default to string + _ => DataType::Utf8, + } +} diff --git a/core/src/sql/db_connection_pool/mod.rs b/core/src/sql/db_connection_pool/mod.rs index 535110e9..b38bf8fe 100644 --- a/core/src/sql/db_connection_pool/mod.rs +++ b/core/src/sql/db_connection_pool/mod.rs @@ -12,6 +12,8 @@ pub mod duckdbpool; pub mod mysqlpool; #[cfg(feature = "odbc")] pub mod odbcpool; +#[cfg(feature = "oracle")] +pub mod oraclepool; #[cfg(feature = "postgres")] pub mod postgrespool; pub mod runtime; diff --git a/core/src/sql/db_connection_pool/oraclepool.rs b/core/src/sql/db_connection_pool/oraclepool.rs new file mode 100644 index 00000000..b3f7e072 --- /dev/null +++ b/core/src/sql/db_connection_pool/oraclepool.rs @@ -0,0 +1,150 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use async_trait::async_trait; +use bb8_oracle::OracleConnectionManager; +use oracle::Connector; + +use secrecy::{ExposeSecret, SecretString}; +use snafu::prelude::*; + +use super::DbConnectionPool; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("Oracle connection failed: {source}"))] + ConnectionError { source: oracle::Error }, + + #[snafu(display("Unable to create Oracle connection pool: {source}"))] + PoolCreationError { source: bb8_oracle::Error }, + + #[snafu(display("Unable to get Oracle connection from pool: {source}"))] + PoolRunError { + source: bb8::RunError, + }, + + #[snafu(display("Missing required parameter: {param}"))] + MissingParameter { param: String }, + + #[snafu(display("Wallet configuration error: {message}"))] + WalletConfigError { message: String }, +} + +pub type Result = std::result::Result; + +pub struct OracleConnectionPool { + pool: Arc>, +} + +impl std::fmt::Debug for OracleConnectionPool { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("OracleConnectionPool").finish() + } +} + +impl OracleConnectionPool { + pub async fn new(params: HashMap) -> Result { + let user = params + .get("user") + .ok_or(Error::MissingParameter { + param: "user".to_string(), + })? + .expose_secret(); + + let password = params + .get("password") + .ok_or(Error::MissingParameter { + param: "password".to_string(), + })? + .expose_secret(); + + let host = params + .get("host") + .ok_or(Error::MissingParameter { + param: "host".to_string(), + })? + .expose_secret(); + + let port = params + .get("port") + .map(|s| s.expose_secret().parse::().unwrap_or(1521)) + .unwrap_or(1521); + + let service_name = params + .get("service_name") + .or_else(|| params.get("sid")) + .map(|s| s.expose_secret().to_string()) + .unwrap_or_else(|| "ORCL".to_string()); + + let connector = Connector::new( + user, + password, + format!("//{}:{}/{}", host, port, service_name), + ); + + // Apply wallet configuration if provided + if let Some(_wallet_path) = params.get("wallet_path") { + // Note: rust-oracle (ODPI-C) handles wallets via TNS_ADMIN or connect string + } + + let manager = OracleConnectionManager::from_connector(connector); + + let pool = bb8::Pool::builder() + .max_size( + params + .get("pool_max") + .and_then(|s| s.expose_secret().parse().ok()) + .unwrap_or(10), + ) + .build(manager) + .await + .map_err(|e| Error::PoolCreationError { source: e })?; + + Ok(Self { + pool: Arc::new(pool), + }) + } + + pub async fn connect_direct( + &self, + ) -> Result { + let conn = Arc::clone(&self.pool) + .get_owned() + .await + .map_err(|e| Error::PoolRunError { source: e })?; + + Ok(super::dbconnection::oracleconn::OracleConnection::new(conn)) + } +} + +#[async_trait] +impl + DbConnectionPool< + bb8::PooledConnection<'static, OracleConnectionManager>, + oracle::sql_type::OracleType, + > for OracleConnectionPool +{ + async fn connect( + &self, + ) -> std::result::Result< + Box< + dyn super::dbconnection::DbConnection< + bb8::PooledConnection<'static, OracleConnectionManager>, + oracle::sql_type::OracleType, + >, + >, + Box, + > { + let conn = Arc::clone(&self.pool).get_owned().await.map_err(|e| { + Box::new(Error::PoolRunError { source: e }) as Box + })?; + + Ok(Box::new( + super::dbconnection::oracleconn::OracleConnection::new(conn), + )) + } + + fn join_push_down(&self) -> super::JoinPushDown { + super::JoinPushDown::Disallow + } +} diff --git a/core/tests/integration.rs b/core/tests/integration.rs index 02dd2041..a0ce8050 100644 --- a/core/tests/integration.rs +++ b/core/tests/integration.rs @@ -10,6 +10,8 @@ mod duckdb; mod flight; #[cfg(feature = "mysql")] mod mysql; +#[cfg(feature = "oracle")] +mod oracle; #[cfg(feature = "postgres")] mod postgres; #[cfg(feature = "sqlite")] diff --git a/core/tests/oracle/common.rs b/core/tests/oracle/common.rs new file mode 100644 index 00000000..5626a5bf --- /dev/null +++ b/core/tests/oracle/common.rs @@ -0,0 +1,106 @@ +use bollard::secret::HealthConfig; +use datafusion_table_providers::sql::db_connection_pool::oraclepool::OracleConnectionPool; +use secrecy::SecretString; +use std::collections::HashMap; +use std::env; + +const DEFAULT_ORACLE_CONTAINER_NAME: &str = "runtime-integration-test-oracle"; +const DEFAULT_ORACLE_IMAGE: &str = "gvenzl/oracle-free:latest"; +const DEFAULT_ORACLE_PASSWORD: &str = "password"; +const DEFAULT_ORACLE_USER: &str = "system"; +const DEFAULT_ORACLE_SERVICE: &str = "FREEPDB1"; + +pub fn get_oracle_params() -> HashMap { + let mut params = HashMap::new(); + + // Default to strict env vars or defaults + let host = env::var("ORACLE_HOST").unwrap_or_else(|_| "localhost".to_string()); + let port = env::var("ORACLE_PORT").unwrap_or_else(|_| "1521".to_string()); + let user = env::var("ORACLE_USER").unwrap_or_else(|_| DEFAULT_ORACLE_USER.to_string()); + let pass = env::var("ORACLE_PASSWORD").unwrap_or_else(|_| DEFAULT_ORACLE_PASSWORD.to_string()); + let service = env::var("ORACLE_SERVICE").unwrap_or_else(|_| DEFAULT_ORACLE_SERVICE.to_string()); + + params.insert("host".to_string(), SecretString::from(host)); + params.insert("port".to_string(), SecretString::from(port)); + params.insert("user".to_string(), SecretString::from(user)); + params.insert("password".to_string(), SecretString::from(pass)); + params.insert("service_name".to_string(), SecretString::from(service)); + + // Optional wallet params + if let Ok(wallet) = env::var("ORACLE_WALLET_PATH") { + params.insert("wallet_path".to_string(), SecretString::from(wallet)); + } + if let Ok(wpass) = env::var("ORACLE_WALLET_PASSWORD") { + params.insert("wallet_password".to_string(), SecretString::from(wpass)); + } + + params +} + +pub async fn get_oracle_connection_pool() -> OracleConnectionPool { + let params = get_oracle_params(); + OracleConnectionPool::new(params) + .await + .expect("Failed to create Oracle connection pool") +} + +pub async fn start_oracle_docker_container( +) -> Result { + let container_name = env::var("ORACLE_CONTAINER_NAME") + .unwrap_or_else(|_| DEFAULT_ORACLE_CONTAINER_NAME.to_string()); + + let oracle_docker_image = std::env::var("ORACLE_DOCKER_IMAGE") + .unwrap_or_else(|_| "gvenzl/oracle-free:latest".to_string()); + + let host_port = env::var("ORACLE_PORT") + .unwrap_or_else(|_| "1521".to_string()) + .parse::() + .unwrap_or(1521); + + let mut builder = crate::docker::ContainerRunnerBuilder::new(container_name) + .image(oracle_docker_image) + .add_port_binding(host_port, 1521) + .healthcheck(HealthConfig { + test: Some(vec![ + "CMD-SHELL".to_string(), + "/usr/local/bin/checkHealth.sh".to_string(), + ]), + interval: Some(1_000_000_000), // 1s + timeout: Some(500_000_000), // 500ms + retries: Some(30), // Give it time to start + start_period: Some(5_000_000_000), // 5s initial wait + start_interval: None, + }); + + // Pass through all ORACLE_ environment variables from the host to the container + for (key, value) in env::vars() { + if key.starts_with("ORACLE_") + && !["ORACLE_DOCKER_IMAGE", "ORACLE_PORT", "ORACLE_HOST"].contains(&key.as_str()) + { + builder = builder.add_env_var(&key, &value); + } + } + + // Ensure we have a password set if not provided in environment, as Oracle images require it + if env::var("ORACLE_PASSWORD").is_err() && env::var("ORACLE_RANDOM_PASSWORD").is_err() { + builder = builder.add_env_var("ORACLE_PASSWORD", DEFAULT_ORACLE_PASSWORD); + } + + // Support creating an app user if ORACLE_USER is set and not a system user + if let Ok(user) = env::var("ORACLE_USER") { + if user.to_uppercase() != "SYSTEM" && user.to_uppercase() != "SYS" { + builder = builder.add_env_var("APP_USER", &user); + + // Prioritize APP_USER_PASSWORD, then ORACLE_PASSWORD, then empty string + let pass = env::var("APP_USER_PASSWORD") + .or_else(|_| env::var("ORACLE_PASSWORD")) + .unwrap_or_else(|_| "".to_string()); + builder = builder.add_env_var("APP_USER_PASSWORD", &pass); + } + } + + let running_container = builder.build()?.run().await?; + + // Let the healthcheck handle readiness + Ok(running_container) +} diff --git a/core/tests/oracle/mod.rs b/core/tests/oracle/mod.rs new file mode 100644 index 00000000..81cc29a8 --- /dev/null +++ b/core/tests/oracle/mod.rs @@ -0,0 +1,593 @@ +use datafusion::arrow::array::*; +use datafusion::arrow::datatypes::{i256, DataType, Field, Schema, TimeUnit}; +use datafusion::arrow::record_batch::RecordBatch; +use datafusion::execution::context::SessionContext; +use datafusion::sql::TableReference; +use datafusion_table_providers::oracle::OracleTableFactory; +use datafusion_table_providers::sql::db_connection_pool::dbconnection::oracleconn::OraclePooledConnection; +use datafusion_table_providers::sql::db_connection_pool::DbConnectionPool; +use datafusion_table_providers::sql::sql_provider_datafusion::SqlTable; +use std::sync::Arc; + +mod common; + +#[tokio::test] +async fn test_oracle_connection_pool() { + let pool = common::get_oracle_connection_pool().await; + let conn = pool + .connect_direct() + .await + .expect("Failed to get connection"); + + let rows = conn + .conn + .query("SELECT 1 FROM DUAL", &[]) + .expect("Failed to execute query"); + let rows: Vec = rows + .collect::, _>>() + .expect("Failed to collect rows"); + assert!(!rows.is_empty()); + + let first_row = &rows[0]; + let val_str: String = first_row.get(0).expect("Value should exist"); + assert_eq!(val_str, "1"); +} + +/// Test registering Oracle's DUAL table as a DataFusion table provider +#[tokio::test] +async fn test_oracle_table_provider_registration() { + let pool = common::get_oracle_connection_pool().await; + let factory = OracleTableFactory::new(Arc::new(pool)); + + let provider = factory + .table_provider(TableReference::from("DUAL")) + .await + .expect("Failed to create table provider"); + + let ctx = SessionContext::new(); + ctx.register_table("dual_test", provider) + .expect("Failed to register table"); + + let df = ctx + .sql("SELECT * FROM dual_test") + .await + .expect("Failed to create dataframe"); + let _result = df.collect().await.expect("Failed to execute query"); +} + +/// Placeholder test for Oracle type mapping +#[tokio::test] +async fn test_oracle_types() { + let pool = common::get_oracle_connection_pool().await; + let _conn = pool + .connect_direct() + .await + .expect("Failed to get connection"); + // Type mapping is tested implicitly via test_oracle_insert_and_read +} + +/// Test querying data through DataFusion using ALL_TABLES system view. +/// Validates the full Provider -> DataFusion query path. +#[tokio::test] +async fn test_oracle_query_with_data() { + let pool = common::get_oracle_connection_pool().await; + let factory = OracleTableFactory::new(Arc::new(pool)); + + let table_name = "ALL_TABLES"; + let provider = factory + .table_provider(TableReference::from(table_name)) + .await + .expect("Failed to create table provider for ALL_TABLES"); + + // Verify schema contains expected columns + let schema = provider.schema(); + let fields: Vec = schema.fields().iter().map(|f| f.name().clone()).collect(); + assert!( + fields.contains(&"TABLE_NAME".to_string()) || fields.contains(&"table_name".to_string()), + "Schema missing 'table_name' column: {:?}", + fields + ); + + let ctx = SessionContext::new(); + ctx.register_table("system_tables", provider) + .expect("Failed to register table"); + + let sql = "SELECT \"TABLE_NAME\", \"OWNER\" FROM system_tables"; + let df = ctx.sql(sql).await.expect("Failed to build plan"); + + let batches = df.collect().await.expect("Query execution failed"); + let row_count: usize = batches.iter().map(|b| b.num_rows()).sum(); + assert!(row_count > 0, "Expected to read rows from ALL_TABLES"); +} + +/// Test schema inference for ALL_TABLES system view +#[tokio::test] +async fn test_oracle_explain_plan() { + let pool = common::get_oracle_connection_pool().await; + let factory = OracleTableFactory::new(Arc::new(pool)); + + let provider = factory + .table_provider(TableReference::from("ALL_TABLES")) + .await + .expect("Failed to create table provider for ALL_TABLES"); + + let schema = provider.schema(); + println!("\n=== ALL_TABLES Schema ==="); + for field in schema.fields() { + println!( + " {} : {:?} (nullable: {})", + field.name(), + field.data_type(), + field.is_nullable() + ); + } + + assert!(schema.fields().len() > 0, "Expected schema with columns"); + + let field_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect(); + assert!( + field_names.contains(&"TABLE_NAME"), + "Expected TABLE_NAME column" + ); + assert!(field_names.contains(&"OWNER"), "Expected OWNER column"); +} + +/// Test schema inference for ALL_TAB_COLUMNS system view +#[tokio::test] +async fn test_oracle_explain_verbose() { + let pool = common::get_oracle_connection_pool().await; + let factory = OracleTableFactory::new(Arc::new(pool)); + + let provider = factory + .table_provider(TableReference::from("ALL_TAB_COLUMNS")) + .await + .expect("Failed to create table provider for ALL_TAB_COLUMNS"); + + let schema = provider.schema(); + println!("\n=== ALL_TAB_COLUMNS Schema ==="); + for field in schema.fields() { + println!(" {} : {:?}", field.name(), field.data_type()); + } + + assert!(schema.fields().len() > 0, "Expected schema with columns"); + + let field_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect(); + assert!( + field_names.contains(&"COLUMN_NAME"), + "Expected COLUMN_NAME column" + ); + assert!( + field_names.contains(&"DATA_TYPE"), + "Expected DATA_TYPE column" + ); +} + +/// Row struct for insertion test +#[derive(Debug)] +struct Row { + id: i64, + name: String, + age: i32, + score: f64, +} + +fn create_sample_rows() -> Vec { + vec![ + Row { + id: 1, + name: "Alice".to_string(), + age: 30, + score: 91.5, + }, + Row { + id: 2, + name: "Bob".to_string(), + age: 45, + score: 85.2, + }, + ] +} + +/// Creates or recreates a test table with the given name +async fn create_test_table( + conn: &oracle::Connection, + table_name: &str, +) -> std::result::Result<(), oracle::Error> { + let check_sql = format!( + "SELECT count(*) FROM user_tables WHERE table_name = '{}'", + table_name + ); + let rows = conn.query(&check_sql, &[])?; + let rows: Vec = rows.collect::, _>>()?; + let count: i64 = if !rows.is_empty() { rows[0].get(0)? } else { 0 }; + + if count > 0 { + let _ = conn.execute(&format!("DROP TABLE {}", table_name), &[]); + } + + let sql = format!( + "CREATE TABLE {} ( + id NUMBER, + name VARCHAR2(100), + age NUMBER, + score BINARY_DOUBLE + )", + table_name + ); + + conn.execute(&sql, &[])?; + Ok(()) +} + +async fn insert_test_rows( + conn: &oracle::Connection, + table_name: &str, + rows: Vec, +) -> std::result::Result<(), oracle::Error> { + let sql = format!( + "INSERT INTO {} (id, name, age, score) VALUES (:1, :2, :3, :4)", + table_name + ); + + for row in rows { + conn.execute(&sql, &[&row.id, &row.name, &row.age, &row.score])?; + } + + conn.commit()?; + Ok(()) +} + +/// Full integration test: Create table -> Insert data -> Read via DataFusion +#[tokio::test] +async fn test_oracle_insert_and_read() { + let table_name = "TEST_EMPLOYEES"; + + let pool = common::get_oracle_connection_pool().await; + let conn = pool + .connect_direct() + .await + .expect("Failed to get connection"); + + create_test_table(&conn.conn, table_name) + .await + .expect("Create table failed"); + insert_test_rows(&conn.conn, table_name, create_sample_rows()) + .await + .expect("Insert failed"); + + drop(conn); + drop(pool); + + let pool_query = common::get_oracle_connection_pool().await; + let factory = OracleTableFactory::new(Arc::new(pool_query)); + + let ctx = SessionContext::new(); + let provider = factory + .table_provider(TableReference::from(table_name)) + .await + .expect("Provider creation failed"); + + // Verify schema has expected columns (uppercase in Oracle) + let schema = provider.schema(); + let fields: Vec = schema.fields().iter().map(|f| f.name().clone()).collect(); + assert!(fields.contains(&"ID".to_string())); + assert!(fields.contains(&"NAME".to_string())); + assert!(fields.contains(&"SCORE".to_string())); + + ctx.register_table("employees", provider) + .expect("Table register failed"); + + // Note: Column names must be quoted for uppercase identifiers in DataFusion SQL + let sql = "SELECT * FROM employees ORDER BY \"ID\""; + let df = ctx.sql(sql).await.expect("Query failed"); + let batches = df.collect().await.expect("Collect failed"); + + let row_count: usize = batches.iter().map(|b| b.num_rows()).sum(); + assert_eq!(row_count, 2); +} + +#[tokio::test] +async fn test_oracle_number_types() { + let create_table_stmt = " + CREATE TABLE number_test_table ( + n1 NUMBER, + n2 NUMBER(10), + n3 NUMBER(10, 2), + n4 NUMBER(38, 10), + n5 NUMBER(38) + ) + "; + let insert_table_stmt = " + INSERT INTO number_test_table (n1, n2, n3, n4, n5) + VALUES ( + 123.456, + 1234567890, + 12345678.90, + 123456789012345678.1234567890, + 12345678901234567890123456789012345678 + ) + "; + + let schema = Arc::new(Schema::new(vec![ + Field::new("N1", DataType::Decimal128(38, 0), true), + Field::new("N2", DataType::Decimal128(10, 0), true), + Field::new("N3", DataType::Decimal128(10, 2), true), + Field::new("N4", DataType::Decimal128(38, 10), true), + Field::new("N5", DataType::Decimal128(38, 0), true), + ])); + + let expected_record = RecordBatch::try_new( + Arc::clone(&schema), + vec![ + Arc::new( + Decimal128Array::from(vec![Some(123)]) + .with_precision_and_scale(38, 0) + .unwrap(), + ), + Arc::new( + Decimal128Array::from(vec![Some(1234567890)]) + .with_precision_and_scale(10, 0) + .unwrap(), + ), + Arc::new( + Decimal128Array::from(vec![Some(1234567890)]) + .with_precision_and_scale(10, 2) + .unwrap(), + ), + Arc::new( + Decimal128Array::from(vec![Some(1234567890123456781234567890)]) + .with_precision_and_scale(38, 10) + .unwrap(), + ), + Arc::new( + Decimal128Array::from(vec![Some(12345678901234567890123456789012345678)]) + .with_precision_and_scale(38, 0) + .unwrap(), + ), + ], + ) + .expect("Failed to create expected record batch"); + + arrow_oracle_one_way( + "NUMBER_TEST_TABLE", + create_table_stmt, + insert_table_stmt, + expected_record, + ) + .await; +} + +async fn arrow_oracle_one_way( + table_name: &str, + create_table_stmt: &str, + insert_table_stmt: &str, + expected_record: RecordBatch, +) -> Vec { + let pool = common::get_oracle_connection_pool().await; + let conn = pool + .connect_direct() + .await + .expect("Failed to get connection"); + + // Cleanup and create table + let _ = conn + .conn + .execute(&format!("DROP TABLE {}", table_name), &[]); + conn.conn + .execute(create_table_stmt, &[]) + .expect("Failed to create table"); + conn.conn + .execute(insert_table_stmt, &[]) + .expect("Failed to insert data"); + conn.conn.commit().expect("Failed to commit"); + + let sqltable_pool: Arc< + dyn DbConnectionPool + + Send + + Sync + + 'static, + > = Arc::new(pool); + let table = SqlTable::new("oracle", &sqltable_pool, table_name) + .await + .expect("Table should be created"); + + let ctx = SessionContext::new(); + ctx.register_table(table_name, Arc::new(table)) + .expect("Table should be registered"); + + let sql = format!("SELECT * FROM {}", table_name); + let df = ctx.sql(&sql).await.expect("Query failed"); + + let record_batches = df.collect().await.expect("Collect failed"); + + assert_eq!(record_batches.len(), 1); + assert_eq!(record_batches[0].schema(), expected_record.schema()); + assert_eq!(record_batches[0], expected_record); + + record_batches +} + +#[tokio::test] +async fn test_oracle_date_time_types() { + let create_table_stmt = " + CREATE TABLE date_time_test_table ( + d1 DATE, + t1 TIMESTAMP, + t2 TIMESTAMP(6), + t3 TIMESTAMP WITH TIME ZONE + ) + "; + let insert_table_stmt = " + INSERT INTO date_time_test_table (d1, t1, t2, t3) + VALUES ( + TO_DATE('2024-09-12 10:00:00', 'YYYY-MM-DD HH24:MI:SS'), + TO_TIMESTAMP('2024-09-12 10:00:00.123', 'YYYY-MM-DD HH24:MI:SS.FF3'), + TO_TIMESTAMP('2024-09-12 10:00:00.123456', 'YYYY-MM-DD HH24:MI:SS.FF6'), + TO_TIMESTAMP_TZ('2024-09-12 10:00:00.123 +00:00', 'YYYY-MM-DD HH24:MI:SS.FF3 TZH:TZM') + ) + "; + + let schema = Arc::new(Schema::new(vec![ + Field::new("D1", DataType::Timestamp(TimeUnit::Microsecond, None), true), + Field::new("T1", DataType::Timestamp(TimeUnit::Microsecond, None), true), + Field::new("T2", DataType::Timestamp(TimeUnit::Microsecond, None), true), + Field::new( + "T3", + DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())), + true, + ), + ])); + + let expected_record = RecordBatch::try_new( + Arc::clone(&schema), + vec![ + Arc::new(TimestampMicrosecondArray::from(vec![1_726_135_200_000_000])), + Arc::new(TimestampMicrosecondArray::from(vec![1_726_135_200_123_000])), + Arc::new(TimestampMicrosecondArray::from(vec![1_726_135_200_123_456])), + Arc::new( + TimestampMicrosecondArray::from(vec![1_726_135_200_123_000]).with_timezone("UTC"), + ), + ], + ) + .expect("Failed to create expected record batch"); + + arrow_oracle_one_way( + "DATE_TIME_TEST_TABLE", + create_table_stmt, + insert_table_stmt, + expected_record, + ) + .await; +} + +#[tokio::test] +async fn test_oracle_binary_types() { + let create_table_stmt = " + CREATE TABLE binary_test_table ( + r1 RAW(10), + r2 RAW(100) + ) + "; + let insert_table_stmt = " + INSERT INTO binary_test_table (r1, r2) + VALUES ( + HEXTORAW('DEADBEEF'), + HEXTORAW('ABCDEF0123456789') + ) + "; + + let schema = Arc::new(Schema::new(vec![ + Field::new("R1", DataType::Binary, true), + Field::new("R2", DataType::Binary, true), + ])); + + let expected_record = RecordBatch::try_new( + Arc::clone(&schema), + vec![ + Arc::new(BinaryArray::from_vec(vec![b"\xDE\xAD\xBE\xEF"])), + Arc::new(BinaryArray::from_vec(vec![ + b"\xAB\xCD\xEF\x01\x23\x45\x67\x89", + ])), + ], + ) + .expect("Failed to create expected record batch"); + + arrow_oracle_one_way( + "BINARY_TEST_TABLE", + create_table_stmt, + insert_table_stmt, + expected_record, + ) + .await; +} + +#[tokio::test] +async fn test_oracle_lob_types() { + let create_table_stmt = " + CREATE TABLE lob_test_table ( + b1 BLOB, + c1 CLOB + ) + "; + let insert_table_stmt = " + INSERT INTO lob_test_table (b1, c1) + VALUES ( + HEXTORAW('0102030405'), + 'Large text content for CLOB' + ) + "; + + let schema = Arc::new(Schema::new(vec![ + Field::new("B1", DataType::LargeBinary, true), + Field::new("C1", DataType::LargeUtf8, true), + ])); + + let expected_record = RecordBatch::try_new( + Arc::clone(&schema), + vec![ + Arc::new(LargeBinaryArray::from_vec(vec![b"\x01\x02\x03\x04\x05"])), + Arc::new(LargeStringArray::from(vec!["Large text content for CLOB"])), + ], + ) + .expect("Failed to create expected record batch"); + + arrow_oracle_one_way( + "LOB_TEST_TABLE", + create_table_stmt, + insert_table_stmt, + expected_record, + ) + .await; +} + +#[tokio::test] +async fn test_oracle_null_handling() { + let create_table_stmt = " + CREATE TABLE null_test_table ( + n1 NUMBER, + d1 DATE, + t1 TIMESTAMP, + r1 RAW(10), + b1 BLOB, + c1 CLOB + ) + "; + let insert_table_stmt = " + INSERT INTO null_test_table (n1, d1, t1, r1, b1, c1) + VALUES (NULL, NULL, NULL, NULL, NULL, NULL) + "; + + let schema = Arc::new(Schema::new(vec![ + Field::new("N1", DataType::Decimal128(38, 0), true), + Field::new("D1", DataType::Timestamp(TimeUnit::Microsecond, None), true), + Field::new("T1", DataType::Timestamp(TimeUnit::Microsecond, None), true), + Field::new("R1", DataType::Binary, true), + Field::new("B1", DataType::LargeBinary, true), + Field::new("C1", DataType::LargeUtf8, true), + ])); + + let expected_record = RecordBatch::try_new( + Arc::clone(&schema), + vec![ + Arc::new( + Decimal128Array::from(vec![Option::::None]) + .with_precision_and_scale(38, 0) + .unwrap(), + ), + Arc::new(TimestampMicrosecondArray::from(vec![Option::::None])), + Arc::new(TimestampMicrosecondArray::from(vec![Option::::None])), + Arc::new(BinaryArray::from_opt_vec(vec![None])), + Arc::new(LargeBinaryArray::from_opt_vec(vec![None])), + Arc::new(LargeStringArray::from(vec![Option::<&str>::None])), + ], + ) + .expect("Failed to create expected record batch"); + + arrow_oracle_one_way( + "NULL_TEST_TABLE", + create_table_stmt, + insert_table_stmt, + expected_record, + ) + .await; +} diff --git a/instantclient-basiclite-linuxx64.zip b/instantclient-basiclite-linuxx64.zip new file mode 100644 index 0000000000000000000000000000000000000000..d34442afd5e22a9c6bb8c93a48b945abc8176be6 GIT binary patch literal 11350209 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