use crate::connection::bulk_copy_state::{ATTENTION_TIMEOUT_SECONDS, BulkCopyTimeoutState};
use crate::connection::metadata_retriever::{FmtOnlyMetadataRetriever, MetadataRetriever};
use crate::connection::tds_client::TdsClient;
use crate::core::TdsResult;
use crate::datatypes::bulk_copy_metadata::{BulkCopyColumnMetadata, SqlDbType};
use crate::error::Error;
use crate::error::SqlInfoMessage;
use crate::error::bulk_copy_errors::{
BulkCopyAttentionTimeoutError, BulkCopyError, BulkCopyTimeoutError,
};
use crate::message::transaction_management::TransactionIsolationLevel;
use async_trait::async_trait;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct BulkCopyOptions {
pub batch_size: usize,
pub timeout_sec: u32,
pub check_constraints: bool,
pub fire_triggers: bool,
pub keep_identity: bool,
pub keep_nulls: bool,
pub table_lock: bool,
pub use_internal_transaction: bool,
pub notification_interval: usize,
pub allow_encrypted_value_modifications: bool,
}
impl Default for BulkCopyOptions {
fn default() -> Self {
Self {
batch_size: 0,
timeout_sec: 30,
check_constraints: false,
fire_triggers: false,
keep_identity: false,
keep_nulls: false,
table_lock: false,
use_internal_transaction: false, notification_interval: 0,
allow_encrypted_value_modifications: false,
}
}
}
impl BulkCopyOptions {
pub fn new() -> Self {
Self::default()
}
pub fn validate(&self) -> TdsResult<()> {
if self.batch_size > 1_000_000 {
return Err(crate::error::Error::UsageError(
"batch_size cannot exceed 1,000,000".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub enum ColumnMappingSource {
Name(String),
Ordinal(usize),
}
#[derive(Debug, Clone)]
pub struct ColumnMapping {
pub source: ColumnMappingSource,
pub destination: String,
}
impl ColumnMapping {
pub fn by_name(source: impl Into<String>, destination: impl Into<String>) -> Self {
Self {
source: ColumnMappingSource::Name(source.into()),
destination: destination.into(),
}
}
pub fn by_ordinal(source_ordinal: usize, destination: impl Into<String>) -> Self {
Self {
source: ColumnMappingSource::Ordinal(source_ordinal),
destination: destination.into(),
}
}
}
#[derive(Debug, Clone)]
pub struct ResolvedColumnMapping {
pub source_index: usize,
pub destination_index: usize,
pub destination_name: String,
pub destination_type: SqlDbType,
}
#[derive(Debug, Clone)]
pub struct BulkCopyProgress {
pub rows_copied: u64,
pub total_rows: Option<u64>,
pub elapsed: Duration,
pub rows_per_second: f64,
}
impl BulkCopyProgress {
pub fn percentage(&self) -> Option<f64> {
self.total_rows
.map(|total| (self.rows_copied as f64 / total as f64) * 100.0)
}
pub fn estimated_time_remaining(&self) -> Option<Duration> {
if self.rows_per_second <= 0.0 {
return None;
}
self.total_rows.map(|total| {
let remaining_rows = total.saturating_sub(self.rows_copied) as f64;
Duration::from_secs_f64(remaining_rows / self.rows_per_second)
})
}
}
#[derive(Debug, Clone)]
pub struct BulkCopyResult {
pub rows_affected: u64,
pub elapsed: Duration,
pub rows_per_second: f64,
}
impl BulkCopyResult {
pub fn new(rows_affected: u64, elapsed: Duration) -> Self {
let rows_per_second = if elapsed.as_secs_f64() > 0.0 {
rows_affected as f64 / elapsed.as_secs_f64()
} else {
0.0
};
Self {
rows_affected,
elapsed,
rows_per_second,
}
}
}
pub struct BulkCopy<'a> {
client: &'a mut TdsClient,
table_name: String,
options: BulkCopyOptions,
column_mappings: Vec<ColumnMapping>,
progress_callback: Option<Box<dyn FnMut(BulkCopyProgress) + Send + 'a>>,
destination_metadata: Option<Vec<BulkCopyColumnMetadata>>,
metadata_retriever: Box<dyn MetadataRetriever + 'a>,
timeout_state: Option<BulkCopyTimeoutState>,
}
impl<'a> BulkCopy<'a> {
pub fn new(client: &'a mut TdsClient, table_name: impl Into<String>) -> Self {
Self {
client,
table_name: table_name.into(),
options: BulkCopyOptions::default(),
column_mappings: Vec::new(),
progress_callback: None,
destination_metadata: None,
metadata_retriever: Box::new(FmtOnlyMetadataRetriever::new()),
timeout_state: None,
}
}
pub fn with_retriever(
client: &'a mut TdsClient,
table_name: impl Into<String>,
retriever: Box<dyn MetadataRetriever + 'a>,
) -> Self {
Self {
client,
table_name: table_name.into(),
options: BulkCopyOptions::default(),
column_mappings: Vec::new(),
progress_callback: None,
destination_metadata: None,
metadata_retriever: retriever,
timeout_state: None,
}
}
pub fn batch_size(mut self, size: usize) -> Self {
self.options.batch_size = size;
self
}
fn duration_to_timeout_seconds(timeout: Duration) -> u32 {
if timeout > Duration::ZERO {
u32::try_from(timeout.as_secs().max(1)).unwrap_or(u32::MAX)
} else {
0
}
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.options.timeout_sec = Self::duration_to_timeout_seconds(timeout);
self
}
pub fn check_constraints(mut self, enabled: bool) -> Self {
self.options.check_constraints = enabled;
self
}
pub fn fire_triggers(mut self, enabled: bool) -> Self {
self.options.fire_triggers = enabled;
self
}
pub fn keep_identity(mut self, enabled: bool) -> Self {
self.options.keep_identity = enabled;
self
}
pub fn keep_nulls(mut self, enabled: bool) -> Self {
self.options.keep_nulls = enabled;
self
}
pub fn allow_encrypted_value_modifications(mut self, enabled: bool) -> Self {
self.options.allow_encrypted_value_modifications = enabled;
self
}
pub fn table_lock(mut self, enabled: bool) -> Self {
self.options.table_lock = enabled;
self
}
pub fn use_internal_transaction(mut self, enabled: bool) -> Self {
self.options.use_internal_transaction = enabled;
self
}
pub fn notification_interval(mut self, interval: usize) -> Self {
self.options.notification_interval = interval;
self
}
pub fn add_column_mapping(mut self, mapping: ColumnMapping) -> Self {
self.column_mappings.push(mapping);
self
}
fn resolve_column_mappings(
&self,
destination_metadata: &[BulkCopyColumnMetadata],
) -> TdsResult<Vec<ResolvedColumnMapping>> {
let mut resolved = Vec::with_capacity(self.column_mappings.len());
for mapping in &self.column_mappings {
let source_index = match &mapping.source {
ColumnMappingSource::Ordinal(idx) => *idx,
ColumnMappingSource::Name(_) => {
return Err(Error::UsageError(
"Source column name mappings are not supported with iterator-based bulk copy".to_string()
));
}
};
let dest_column = destination_metadata
.iter()
.enumerate()
.find(|(_, col)| col.column_name == mapping.destination)
.ok_or_else(|| {
Error::UsageError(format!(
"Destination column '{}' not found in table",
mapping.destination
))
})?;
resolved.push(ResolvedColumnMapping {
source_index,
destination_index: dest_column.0,
destination_name: dest_column.1.column_name.clone(),
destination_type: dest_column.1.sql_type,
});
}
resolved.sort_by_key(|m| m.destination_index);
Ok(resolved)
}
fn validate_transaction_state(&self) -> TdsResult<()> {
let has_active_transaction = self.client.has_active_transaction();
let use_internal_transaction = self.options.use_internal_transaction;
if use_internal_transaction && has_active_transaction {
return Err(Error::UsageError(
"Cannot use UseInternalTransaction when the connection already has an active transaction. \
Either commit/rollback the existing transaction, or set use_internal_transaction to false \
to participate in the existing transaction.".to_string()
));
}
Ok(())
}
pub async fn get_resolved_mappings(&mut self) -> TdsResult<Vec<ResolvedColumnMapping>> {
let destination_metadata = self.retrieve_destination_metadata().await?;
self.resolve_column_mappings(&destination_metadata)
}
pub fn on_progress<F>(mut self, callback: F) -> Self
where
F: FnMut(BulkCopyProgress) + Send + 'a,
{
self.progress_callback = Some(Box::new(callback));
self
}
pub async fn retrieve_destination_metadata(
&mut self,
) -> TdsResult<Vec<BulkCopyColumnMetadata>> {
if let Some(ref metadata) = self.destination_metadata {
return Ok(metadata.clone());
}
let metadata = self
.metadata_retriever
.retrieve_metadata(self.client, &self.table_name, self.options.timeout_sec)
.await?;
self.destination_metadata = Some(metadata.clone());
Ok(metadata)
}
pub async fn write_to_server_zerocopy<I, R>(&mut self, rows: I) -> TdsResult<BulkCopyResult>
where
I: IntoIterator<Item = R>,
R: BulkLoadRow,
{
let start_time = Instant::now();
let mut total_rows = 0u64;
self.timeout_state = Some(BulkCopyTimeoutState::from_seconds(self.options.timeout_sec));
self.validate_transaction_state()?;
let mut rows = rows.into_iter().peekable();
if self.destination_metadata.is_none() {
self.retrieve_destination_metadata().await?;
}
let _server_metadata = self
.destination_metadata
.as_ref()
.ok_or_else(|| Error::UsageError("Destination metadata not available".to_string()))?;
if self.column_mappings.is_empty() {
let destination_metadata = self.retrieve_destination_metadata().await?;
let filtered_metadata: Vec<_> = if self.options.keep_identity {
destination_metadata.clone()
} else {
destination_metadata
.iter()
.filter(|col| !col.is_identity)
.cloned()
.collect()
};
let source_column_count = if let Some(_first_row) = rows.peek() {
filtered_metadata.len()
} else {
0
};
let mapping_count = std::cmp::min(source_column_count, filtered_metadata.len());
self.column_mappings.reserve(mapping_count);
for (i, col) in filtered_metadata.iter().enumerate().take(mapping_count) {
self.column_mappings
.push(ColumnMapping::by_ordinal(i, col.column_name.clone()));
}
}
if rows.peek().is_some() {
let destination_metadata = self.retrieve_destination_metadata().await?;
let filtered_metadata: Vec<_> = if self.options.keep_identity {
destination_metadata.clone()
} else {
destination_metadata
.iter()
.filter(|col| !col.is_identity)
.cloned()
.collect()
};
let resolved_mappings = self.resolve_column_mappings(&filtered_metadata)?;
let dest_column_metadata = filtered_metadata;
self.write_rows_to_server_zerocopy(
rows,
&resolved_mappings,
dest_column_metadata,
&mut total_rows,
start_time,
)
.await?;
}
let elapsed = start_time.elapsed();
Ok(BulkCopyResult::new(total_rows, elapsed))
}
async fn write_rows_to_server_zerocopy<I, R>(
&mut self,
mut rows: std::iter::Peekable<I>,
resolved_mappings: &[ResolvedColumnMapping],
dest_column_metadata: Vec<BulkCopyColumnMetadata>,
total_rows: &mut u64,
start_time: Instant,
) -> TdsResult<()>
where
I: Iterator<Item = R>,
R: BulkLoadRow,
{
let batch_size = if self.options.batch_size == 0 {
usize::MAX
} else {
self.options.batch_size
};
let timeout_sec = if self.options.timeout_sec > 0 {
Some(self.options.timeout_sec)
} else {
None
};
let use_internal_transaction = self.options.use_internal_transaction;
let mut accumulated_info: Vec<SqlInfoMessage> = Vec::new();
loop {
if rows.peek().is_none() {
break;
}
if let Some(ref mut state) = self.timeout_state
&& state.is_expired()
&& !state.is_attention_sent()
{
state.mark_bulk_copy_write_timeout();
state.begin_sending_attention();
let attention_timeout = Duration::from_secs(ATTENTION_TIMEOUT_SECONDS);
let ack_received = self
.client
.send_attention_with_timeout(attention_timeout)
.await
.unwrap_or(false);
state.mark_attention_sent();
if ack_received {
state.mark_attention_received();
let err = BulkCopyTimeoutError::new(
*total_rows,
self.options.timeout_sec,
Some("Bulk copy operation timed out".to_string()),
);
return Err(Error::BulkCopyError(BulkCopyError::Timeout(err)));
} else {
let _ = self.client.close_connection().await;
let err = BulkCopyAttentionTimeoutError::new(true);
return Err(Error::BulkCopyError(BulkCopyError::AttentionTimeout(err)));
}
}
let batch_iter = (&mut rows).take(batch_size);
if use_internal_transaction {
self.client
.begin_transaction(TransactionIsolationLevel::ReadCommitted, None)
.await?;
}
let batch_result = self
.client
.execute_bulk_load_streaming_zerocopy(
self.table_name.clone(),
dest_column_metadata.clone(),
self.options.clone(),
timeout_sec,
None,
batch_iter,
resolved_mappings,
)
.await;
match batch_result {
Ok(batch_count) => {
accumulated_info.extend(self.client.take_info_messages());
if use_internal_transaction
&& let Err(e) = self.client.commit_transaction(None, None).await
{
let _ = self.client.take_info_messages();
self.client
.extend_info_messages(std::mem::take(&mut accumulated_info));
return Err(e);
}
*total_rows += batch_count;
}
Err(e) => {
accumulated_info.extend(self.client.take_info_messages());
if use_internal_transaction && self.client.has_active_transaction() {
let _ = self.client.rollback_transaction(None, None).await;
}
let _ = self.client.take_info_messages();
self.client
.extend_info_messages(std::mem::take(&mut accumulated_info));
return Err(e);
}
}
if let Some(ref mut callback) = self.progress_callback
&& self.options.notification_interval > 0
&& (*total_rows).is_multiple_of(self.options.notification_interval as u64)
{
let elapsed = start_time.elapsed();
let rows_per_second = if elapsed.as_secs_f64() > 0.0 {
*total_rows as f64 / elapsed.as_secs_f64()
} else {
0.0
};
callback(BulkCopyProgress {
rows_copied: *total_rows,
total_rows: None,
elapsed,
rows_per_second,
});
}
}
let _ = self.client.take_info_messages();
self.client.extend_info_messages(accumulated_info);
Ok(())
}
pub fn is_timed_out(&self) -> bool {
self.timeout_state
.as_ref()
.is_some_and(|state| state.is_expired())
}
pub fn remaining_timeout_ms(&self) -> Option<u64> {
self.timeout_state
.as_ref()
.and_then(|state| state.remaining_ms())
}
}
#[async_trait]
pub trait BulkLoadRow {
async fn write_to_packet(
&self,
writer: &mut crate::message::bulk_load::StreamingBulkLoadWriter<'_>,
column_index: &mut usize,
) -> TdsResult<()>;
}
#[cfg(test)]
mod tests {
use crate::datatypes::bulk_copy_metadata::SystemTypeId;
use super::*;
#[test]
fn test_bulk_copy_options_default() {
let opts = BulkCopyOptions::default();
assert_eq!(opts.batch_size, 0);
assert_eq!(opts.timeout_sec, 30);
assert!(!opts.check_constraints);
assert!(!opts.fire_triggers);
assert!(!opts.keep_identity);
assert!(!opts.keep_nulls);
assert!(!opts.table_lock);
assert!(!opts.use_internal_transaction); assert_eq!(opts.notification_interval, 0);
}
#[test]
fn test_bulk_copy_options_validate() {
let opts = BulkCopyOptions {
batch_size: 5000,
..Default::default()
};
assert!(opts.validate().is_ok());
let invalid_opts = BulkCopyOptions {
batch_size: 2_000_000,
..Default::default()
};
assert!(invalid_opts.validate().is_err());
}
#[test]
fn test_column_mapping_by_name() {
let mapping = ColumnMapping::by_name("source_col", "dest_col");
assert!(matches!(
mapping.source,
ColumnMappingSource::Name(ref name) if name == "source_col"
));
assert_eq!(mapping.destination, "dest_col");
}
#[test]
fn test_column_mapping_by_ordinal() {
let mapping = ColumnMapping::by_ordinal(2, "dest_col");
assert!(matches!(mapping.source, ColumnMappingSource::Ordinal(2)));
assert_eq!(mapping.destination, "dest_col");
}
#[test]
fn test_bulk_copy_progress_percentage() {
let progress = BulkCopyProgress {
rows_copied: 500,
total_rows: Some(1000),
elapsed: Duration::from_secs(10),
rows_per_second: 50.0,
};
assert_eq!(progress.percentage(), Some(50.0));
let progress_no_total = BulkCopyProgress {
rows_copied: 500,
total_rows: None,
elapsed: Duration::from_secs(10),
rows_per_second: 50.0,
};
assert_eq!(progress_no_total.percentage(), None);
}
#[test]
fn test_bulk_copy_progress_estimated_time() {
let progress = BulkCopyProgress {
rows_copied: 1000,
total_rows: Some(2000),
elapsed: Duration::from_secs(10),
rows_per_second: 100.0,
};
let estimated = progress.estimated_time_remaining().unwrap();
assert_eq!(estimated.as_secs(), 10); }
#[test]
fn test_bulk_copy_result() {
let result = BulkCopyResult::new(10000, Duration::from_secs(10));
assert_eq!(result.rows_affected, 10000);
assert_eq!(result.rows_per_second, 1000.0);
}
#[test]
fn test_bulk_copy_metadata_creation() {
use crate::datatypes::bulk_copy_metadata::TypeLength;
let bulk_meta = BulkCopyColumnMetadata::new("TestColumn", SqlDbType::Int, 0x26)
.with_length(4, TypeLength::Fixed(4))
.with_nullable(true);
assert_eq!(bulk_meta.column_name, "TestColumn");
assert_eq!(bulk_meta.sql_type, SqlDbType::Int);
assert_eq!(bulk_meta.tds_type, 0x26); assert!(bulk_meta.is_nullable);
}
#[test]
fn test_system_type_id_to_sql_db_type_conversion() {
assert_eq!(
SqlDbType::try_from(SystemTypeId(48)).unwrap(),
SqlDbType::TinyInt
);
assert_eq!(
SqlDbType::try_from(SystemTypeId(56)).unwrap(),
SqlDbType::Int
);
assert_eq!(
SqlDbType::try_from(SystemTypeId(127)).unwrap(),
SqlDbType::BigInt
);
assert_eq!(
SqlDbType::try_from(SystemTypeId(231)).unwrap(),
SqlDbType::NVarChar
);
assert_eq!(
SqlDbType::try_from(SystemTypeId(167)).unwrap(),
SqlDbType::VarChar
);
assert_eq!(
SqlDbType::try_from(SystemTypeId(36)).unwrap(),
SqlDbType::UniqueIdentifier
);
assert!(SqlDbType::try_from(SystemTypeId(255)).is_err());
}
fn check_compat(source: SqlDbType, dest: SqlDbType) -> bool {
if source == dest {
return true;
}
matches!(
(source, dest),
(SqlDbType::TinyInt, SqlDbType::SmallInt | SqlDbType::Int | SqlDbType::BigInt) |
(SqlDbType::SmallInt, SqlDbType::Int | SqlDbType::BigInt) |
(SqlDbType::Int, SqlDbType::BigInt) |
(SqlDbType::TinyInt | SqlDbType::SmallInt | SqlDbType::Int | SqlDbType::BigInt,
SqlDbType::Real | SqlDbType::Float) |
(SqlDbType::Real, SqlDbType::Float) |
(SqlDbType::Decimal, SqlDbType::Numeric) | (SqlDbType::Numeric, SqlDbType::Decimal) |
(SqlDbType::Char, SqlDbType::VarChar) |
(SqlDbType::NChar, SqlDbType::NVarChar) |
(SqlDbType::VarChar, SqlDbType::NVarChar) |
(SqlDbType::Text, SqlDbType::VarChar | SqlDbType::NVarChar) |
(SqlDbType::NText, SqlDbType::NVarChar) |
(SqlDbType::VarChar, SqlDbType::Text) |
(SqlDbType::NVarChar, SqlDbType::NText) |
(SqlDbType::Binary, SqlDbType::VarBinary) |
(SqlDbType::VarBinary, SqlDbType::Image) |
(SqlDbType::Image, SqlDbType::VarBinary) |
(SqlDbType::SmallDateTime, SqlDbType::DateTime | SqlDbType::DateTime2) |
(SqlDbType::DateTime, SqlDbType::DateTime2) |
(SqlDbType::Date, SqlDbType::DateTime | SqlDbType::DateTime2) |
(SqlDbType::SmallMoney, SqlDbType::Money)
)
}
#[test]
fn test_type_compatibility_exact_match() {
assert!(check_compat(SqlDbType::Int, SqlDbType::Int));
assert!(check_compat(SqlDbType::NVarChar, SqlDbType::NVarChar));
assert!(check_compat(SqlDbType::DateTime2, SqlDbType::DateTime2));
}
#[test]
fn test_type_compatibility_numeric_promotions() {
assert!(check_compat(SqlDbType::TinyInt, SqlDbType::SmallInt));
assert!(check_compat(SqlDbType::TinyInt, SqlDbType::Int));
assert!(check_compat(SqlDbType::TinyInt, SqlDbType::BigInt));
assert!(check_compat(SqlDbType::SmallInt, SqlDbType::Int));
assert!(check_compat(SqlDbType::SmallInt, SqlDbType::BigInt));
assert!(check_compat(SqlDbType::Int, SqlDbType::BigInt));
assert!(!check_compat(SqlDbType::BigInt, SqlDbType::Int));
assert!(!check_compat(SqlDbType::Int, SqlDbType::SmallInt));
}
#[test]
fn test_type_compatibility_numeric_to_float() {
assert!(check_compat(SqlDbType::TinyInt, SqlDbType::Real));
assert!(check_compat(SqlDbType::SmallInt, SqlDbType::Float));
assert!(check_compat(SqlDbType::Int, SqlDbType::Float));
assert!(check_compat(SqlDbType::BigInt, SqlDbType::Real));
assert!(check_compat(SqlDbType::Real, SqlDbType::Float));
assert!(!check_compat(SqlDbType::Float, SqlDbType::Real));
}
#[test]
fn test_type_compatibility_string_types() {
assert!(check_compat(SqlDbType::Char, SqlDbType::VarChar));
assert!(check_compat(SqlDbType::NChar, SqlDbType::NVarChar));
assert!(check_compat(SqlDbType::VarChar, SqlDbType::NVarChar));
assert!(check_compat(SqlDbType::Text, SqlDbType::VarChar));
assert!(check_compat(SqlDbType::Text, SqlDbType::NVarChar));
assert!(check_compat(SqlDbType::NText, SqlDbType::NVarChar));
assert!(check_compat(SqlDbType::VarChar, SqlDbType::Text));
assert!(check_compat(SqlDbType::NVarChar, SqlDbType::NText));
}
#[test]
fn test_type_compatibility_binary_types() {
assert!(check_compat(SqlDbType::Binary, SqlDbType::VarBinary));
assert!(check_compat(SqlDbType::VarBinary, SqlDbType::Image));
assert!(check_compat(SqlDbType::Image, SqlDbType::VarBinary));
}
#[test]
fn test_type_compatibility_datetime_types() {
assert!(check_compat(SqlDbType::SmallDateTime, SqlDbType::DateTime));
assert!(check_compat(SqlDbType::SmallDateTime, SqlDbType::DateTime2));
assert!(check_compat(SqlDbType::DateTime, SqlDbType::DateTime2));
assert!(check_compat(SqlDbType::Date, SqlDbType::DateTime));
assert!(check_compat(SqlDbType::Date, SqlDbType::DateTime2));
}
#[test]
fn test_type_compatibility_incompatible_types() {
assert!(!check_compat(SqlDbType::VarChar, SqlDbType::Int));
assert!(!check_compat(SqlDbType::NVarChar, SqlDbType::BigInt));
assert!(!check_compat(SqlDbType::Int, SqlDbType::VarChar));
assert!(!check_compat(SqlDbType::DateTime, SqlDbType::Int));
assert!(!check_compat(SqlDbType::VarBinary, SqlDbType::VarChar));
}
#[test]
fn test_use_internal_transaction_default_is_false() {
let opts = BulkCopyOptions::default();
assert!(
!opts.use_internal_transaction,
"use_internal_transaction should default to false (matching .NET)"
);
}
#[test]
fn test_batch_size_zero_means_single_batch() {
let opts = BulkCopyOptions::default();
assert_eq!(opts.batch_size, 0, "batch_size should default to 0");
let effective_batch_size = if opts.batch_size == 0 {
usize::MAX
} else {
opts.batch_size
};
assert_eq!(
effective_batch_size,
usize::MAX,
"batch_size=0 should mean single batch (usize::MAX)"
);
}
#[test]
fn test_batch_size_positive_value() {
let opts = BulkCopyOptions {
batch_size: 1000,
..Default::default()
};
assert_eq!(opts.batch_size, 1000);
let effective_batch_size = if opts.batch_size == 0 {
usize::MAX
} else {
opts.batch_size
};
assert_eq!(effective_batch_size, 1000);
}
#[test]
fn test_use_internal_transaction_can_be_enabled() {
let opts = BulkCopyOptions {
use_internal_transaction: true,
..Default::default()
};
assert!(
opts.use_internal_transaction,
"use_internal_transaction should be configurable to true"
);
}
#[test]
fn test_duration_to_timeout_seconds() {
assert_eq!(BulkCopy::duration_to_timeout_seconds(Duration::ZERO), 0);
assert_eq!(
BulkCopy::duration_to_timeout_seconds(Duration::from_millis(1)),
1
);
assert_eq!(
BulkCopy::duration_to_timeout_seconds(Duration::from_millis(500)),
1
);
assert_eq!(
BulkCopy::duration_to_timeout_seconds(Duration::from_millis(999)),
1
);
assert_eq!(
BulkCopy::duration_to_timeout_seconds(Duration::from_secs(1)),
1
);
assert_eq!(
BulkCopy::duration_to_timeout_seconds(Duration::from_secs(2)),
2
);
assert_eq!(
BulkCopy::duration_to_timeout_seconds(Duration::from_millis(2500)),
2
);
assert_eq!(
BulkCopy::duration_to_timeout_seconds(Duration::from_secs(u32::MAX as u64)),
u32::MAX
);
assert_eq!(
BulkCopy::duration_to_timeout_seconds(Duration::from_secs(1_u64 << 32)),
u32::MAX
);
}
}