#![allow(clippy::result_large_err)] mod common;
use common::session;
use arrow::array::{BinaryArray, BooleanArray, Float64Array, Int8Array, Int64Array, StringArray};
use spark_connect::prelude::*;
use spark_connect::{LiteralExpression, Result};
#[tokio::test]
async fn binds_each_supported_rust_type() -> Result<()> {
let session = session().await?;
let batches = session
.query("SELECT ? AS small, ? AS big, ? AS ratio, ? AS flag, ? AS name, ? AS blob")
.bind(7_i8)
.bind(9_000_000_000_i64)
.bind(0.5_f64)
.bind(true)
.bind("hello".to_string())
.bind(vec![1_u8, 2, 3])
.execute()
.await?;
let batch = &batches[0];
assert_eq!(batch.num_rows(), 1);
let column = |i: usize| batch.column(i).as_any();
assert_eq!(column(0).downcast_ref::<Int8Array>().unwrap().value(0), 7);
assert_eq!(column(1).downcast_ref::<Int64Array>().unwrap().value(0), 9_000_000_000);
assert_eq!(column(2).downcast_ref::<Float64Array>().unwrap().value(0), 0.5);
assert!(column(3).downcast_ref::<BooleanArray>().unwrap().value(0));
assert_eq!(column(4).downcast_ref::<StringArray>().unwrap().value(0), "hello");
assert_eq!(column(5).downcast_ref::<BinaryArray>().unwrap().value(0), &[1, 2, 3]);
Ok(())
}
#[tokio::test]
async fn binds_a_literal_expression_directly() -> Result<()> {
let session = session().await?;
let batches = session
.query("SELECT ? AS price")
.bind(LiteralExpression::Decimal { value: "1.50".into(), precision: 3, scale: 2 })
.execute()
.await?;
let schema = batches[0].schema();
assert_eq!(schema.field(0).data_type().to_string(), "Decimal128(3, 2)");
Ok(())
}
#[tokio::test]
async fn binds_values_without_interpolating_sql() -> Result<()> {
let session = session().await?;
let hostile = "'; DROP TABLE users; --";
let batches = session.query("SELECT ? AS text").bind(hostile).execute().await?;
let text = batches[0].column(0).as_any().downcast_ref::<StringArray>().unwrap();
assert_eq!(text.value(0), hostile);
Ok(())
}
#[tokio::test]
async fn reports_analysis_errors_with_their_condition() -> Result<()> {
let session = session().await?;
let error = session
.sql("SELECT * FROM a_table_that_does_not_exist")?
.collect_async()
.await
.expect_err("querying a missing table should fail");
assert_eq!(error.get_condition().as_deref(), Some("TABLE_OR_VIEW_NOT_FOUND"));
assert!(error.message().contains("a_table_that_does_not_exist"), "{}", error.message());
Ok(())
}
#[tokio::test]
async fn resumes_panics_on_the_calling_task() -> Result<()> {
let session = session().await?;
let joined = tokio::spawn(async move { session.run::<_, ()>(|_| panic!("boom")).await }).await;
let error = joined.expect_err("the panic should have propagated");
assert!(error.is_panic());
Ok(())
}
#[cfg(feature = "chrono")]
#[tokio::test]
async fn binds_chrono_dates_and_timestamps() -> Result<()> {
use arrow::array::{Date32Array, TimestampMicrosecondArray};
use chrono::{NaiveDate, NaiveDateTime};
let session = session().await?;
let date = NaiveDate::from_ymd_opt(2026, 9, 21).unwrap();
let timestamp: NaiveDateTime = date.and_hms_opt(12, 30, 0).unwrap();
let batches = session
.query("SELECT ? AS day, ? AS moment")
.bind(date)
.bind(timestamp)
.execute()
.await?;
let batch = &batches[0];
let days = batch.column(0).as_any().downcast_ref::<Date32Array>().unwrap();
assert_eq!(days.value_as_date(0), Some(date));
let moments = batch.column(1).as_any().downcast_ref::<TimestampMicrosecondArray>().unwrap();
assert_eq!(moments.value(0), timestamp.and_utc().timestamp_micros());
Ok(())
}