use crate::error::{Error, Result};
use crate::row::Value;
use crate::statement::Statement;
#[derive(Debug, Clone, Default)]
pub struct BatchOptions {
pub batch_errors: bool,
pub array_dml_row_counts: bool,
pub auto_commit: bool,
}
impl BatchOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_batch_errors(mut self) -> Self {
self.batch_errors = true;
self
}
pub fn with_row_counts(mut self) -> Self {
self.array_dml_row_counts = true;
self
}
pub fn with_auto_commit(mut self) -> Self {
self.auto_commit = true;
self
}
}
#[derive(Debug, Clone)]
pub struct BatchBinds {
pub(crate) sql: String,
pub(crate) statement: Statement,
pub(crate) rows: Vec<Vec<Value>>,
pub(crate) num_columns: usize,
pub(crate) options: BatchOptions,
}
impl BatchBinds {
pub fn new(sql: impl Into<String>) -> Self {
let sql = sql.into();
let statement = Statement::new(&sql);
Self {
sql,
statement,
rows: Vec::new(),
num_columns: 0,
options: BatchOptions::default(),
}
}
pub fn add_row(&mut self, values: Vec<Value>) -> &mut Self {
if self.rows.is_empty() {
self.num_columns = values.len();
}
self.rows.push(values);
self
}
pub fn with_options(&mut self, options: BatchOptions) -> &mut Self {
self.options = options;
self
}
pub fn row_count(&self) -> usize {
self.rows.len()
}
pub fn column_count(&self) -> usize {
self.num_columns
}
pub fn sql(&self) -> &str {
&self.sql
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn validate(&self) -> Result<()> {
if self.rows.is_empty() {
return Err(Error::Internal("Batch has no rows".to_string()));
}
for (i, row) in self.rows.iter().enumerate() {
if row.len() != self.num_columns {
return Err(Error::Internal(format!(
"Row {} has {} columns, expected {}",
i,
row.len(),
self.num_columns
)));
}
}
Ok(())
}
}
#[derive(Debug)]
pub struct BatchBuilder {
batch: BatchBinds,
}
impl BatchBuilder {
pub fn new(sql: impl Into<String>) -> Self {
Self {
batch: BatchBinds::new(sql),
}
}
pub fn add_row(mut self, values: Vec<Value>) -> Self {
self.batch.add_row(values);
self
}
pub fn add_rows(mut self, rows: Vec<Vec<Value>>) -> Self {
for row in rows {
self.batch.add_row(row);
}
self
}
pub fn with_batch_errors(mut self) -> Self {
self.batch.options.batch_errors = true;
self
}
pub fn with_row_counts(mut self) -> Self {
self.batch.options.array_dml_row_counts = true;
self
}
pub fn with_auto_commit(mut self) -> Self {
self.batch.options.auto_commit = true;
self
}
pub fn build(self) -> BatchBinds {
self.batch
}
}
#[derive(Debug, Clone)]
pub struct BatchResult {
pub row_counts: Option<Vec<u64>>,
pub total_rows_affected: u64,
pub errors: Vec<BatchError>,
pub success_count: usize,
pub failure_count: usize,
}
impl BatchResult {
pub fn new() -> Self {
Self {
row_counts: None,
total_rows_affected: 0,
errors: Vec::new(),
success_count: 0,
failure_count: 0,
}
}
pub fn with_row_counts(counts: Vec<u64>) -> Self {
let total: u64 = counts.iter().sum();
let success_count = counts.len();
Self {
row_counts: Some(counts),
total_rows_affected: total,
errors: Vec::new(),
success_count,
failure_count: 0,
}
}
pub fn is_success(&self) -> bool {
self.errors.is_empty()
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
}
impl Default for BatchResult {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct BatchError {
pub row_index: usize,
pub code: u32,
pub message: String,
}
impl BatchError {
pub fn new(row_index: usize, code: u32, message: impl Into<String>) -> Self {
Self {
row_index,
code,
message: message.into(),
}
}
}
impl std::fmt::Display for BatchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Row {}: ORA-{:05}: {}",
self.row_index, self.code, self.message
)
}
}
impl std::error::Error for BatchError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_batch_builder() {
let batch = BatchBuilder::new("INSERT INTO t (a, b) VALUES (:1, :2)")
.add_row(vec![Value::Integer(1), Value::String("a".to_string())])
.add_row(vec![Value::Integer(2), Value::String("b".to_string())])
.build();
assert_eq!(batch.row_count(), 2);
assert_eq!(batch.column_count(), 2);
assert!(!batch.is_empty());
}
#[test]
fn test_batch_validation() {
let mut batch = BatchBinds::new("INSERT INTO t (a) VALUES (:1)");
batch.add_row(vec![Value::Integer(1)]);
batch.add_row(vec![Value::Integer(2)]);
assert!(batch.validate().is_ok());
}
#[test]
fn test_batch_validation_empty() {
let batch = BatchBinds::new("INSERT INTO t (a) VALUES (:1)");
assert!(batch.validate().is_err());
}
#[test]
fn test_batch_options() {
let opts = BatchOptions::new()
.with_batch_errors()
.with_row_counts()
.with_auto_commit();
assert!(opts.batch_errors);
assert!(opts.array_dml_row_counts);
assert!(opts.auto_commit);
}
#[test]
fn test_batch_result() {
let result = BatchResult::with_row_counts(vec![1, 2, 3]);
assert_eq!(result.total_rows_affected, 6);
assert_eq!(result.success_count, 3);
assert!(result.is_success());
assert!(!result.has_errors());
}
#[test]
fn test_batch_error_display() {
let err = BatchError::new(5, 1, "test error");
assert_eq!(err.to_string(), "Row 5: ORA-00001: test error");
}
#[test]
fn test_add_multiple_rows() {
let batch = BatchBuilder::new("INSERT INTO t (a) VALUES (:1)")
.add_rows(vec![
vec![Value::Integer(1)],
vec![Value::Integer(2)],
vec![Value::Integer(3)],
])
.build();
assert_eq!(batch.row_count(), 3);
}
}