oracle_rs/
batch.rs

1//! Batch execution support for Oracle connections
2//!
3//! This module provides types and methods for executing statements multiple times
4//! with different bind values efficiently (executemany pattern).
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use oracle_rs::{Connection, BatchBuilder};
10//!
11//! let conn = Connection::connect("localhost:1521/ORCLPDB1", "user", "pass").await?;
12//!
13//! // Create a batch for inserting multiple rows
14//! let batch = BatchBuilder::new("INSERT INTO users (id, name) VALUES (:1, :2)")
15//!     .add_row(vec![Value::Integer(1), Value::String("Alice".to_string())])
16//!     .add_row(vec![Value::Integer(2), Value::String("Bob".to_string())])
17//!     .add_row(vec![Value::Integer(3), Value::String("Charlie".to_string())])
18//!     .build();
19//!
20//! let result = conn.execute_batch(&batch).await?;
21//! println!("Rows affected per statement: {:?}", result.row_counts);
22//! ```
23
24use crate::error::{Error, Result};
25use crate::row::Value;
26use crate::statement::Statement;
27
28/// Options for batch execution
29#[derive(Debug, Clone, Default)]
30pub struct BatchOptions {
31    /// Whether to continue execution after errors (batch errors mode)
32    pub batch_errors: bool,
33    /// Whether to return row counts for each statement
34    pub array_dml_row_counts: bool,
35    /// Whether to commit after successful completion
36    pub auto_commit: bool,
37}
38
39impl BatchOptions {
40    /// Create default batch options
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// Enable batch errors mode (continue on errors)
46    pub fn with_batch_errors(mut self) -> Self {
47        self.batch_errors = true;
48        self
49    }
50
51    /// Enable array DML row counts
52    pub fn with_row_counts(mut self) -> Self {
53        self.array_dml_row_counts = true;
54        self
55    }
56
57    /// Enable auto-commit
58    pub fn with_auto_commit(mut self) -> Self {
59        self.auto_commit = true;
60        self
61    }
62}
63
64/// A collection of bind values for batch execution
65#[derive(Debug, Clone)]
66pub struct BatchBinds {
67    /// The SQL statement
68    pub(crate) sql: String,
69    /// Parsed statement information
70    pub(crate) statement: Statement,
71    /// Rows of bind values (each row is one execution)
72    pub(crate) rows: Vec<Vec<Value>>,
73    /// Number of columns per row
74    pub(crate) num_columns: usize,
75    /// Execution options
76    pub(crate) options: BatchOptions,
77}
78
79impl BatchBinds {
80    /// Create a new batch with the given SQL statement
81    pub fn new(sql: impl Into<String>) -> Self {
82        let sql = sql.into();
83        let statement = Statement::new(&sql);
84        Self {
85            sql,
86            statement,
87            rows: Vec::new(),
88            num_columns: 0,
89            options: BatchOptions::default(),
90        }
91    }
92
93    /// Add a row of bind values
94    pub fn add_row(&mut self, values: Vec<Value>) -> &mut Self {
95        if self.rows.is_empty() {
96            self.num_columns = values.len();
97        }
98        self.rows.push(values);
99        self
100    }
101
102    /// Set batch execution options
103    pub fn with_options(&mut self, options: BatchOptions) -> &mut Self {
104        self.options = options;
105        self
106    }
107
108    /// Get the number of rows (executions)
109    pub fn row_count(&self) -> usize {
110        self.rows.len()
111    }
112
113    /// Get the number of bind columns per row
114    pub fn column_count(&self) -> usize {
115        self.num_columns
116    }
117
118    /// Get the SQL statement
119    pub fn sql(&self) -> &str {
120        &self.sql
121    }
122
123    /// Check if batch is empty
124    pub fn is_empty(&self) -> bool {
125        self.rows.is_empty()
126    }
127
128    /// Validate that all rows have the same number of columns
129    pub fn validate(&self) -> Result<()> {
130        if self.rows.is_empty() {
131            return Err(Error::Internal("Batch has no rows".to_string()));
132        }
133
134        for (i, row) in self.rows.iter().enumerate() {
135            if row.len() != self.num_columns {
136                return Err(Error::Internal(format!(
137                    "Row {} has {} columns, expected {}",
138                    i,
139                    row.len(),
140                    self.num_columns
141                )));
142            }
143        }
144
145        Ok(())
146    }
147}
148
149/// Builder for creating batch execution requests
150#[derive(Debug)]
151pub struct BatchBuilder {
152    batch: BatchBinds,
153}
154
155impl BatchBuilder {
156    /// Create a new batch builder with the given SQL statement
157    pub fn new(sql: impl Into<String>) -> Self {
158        Self {
159            batch: BatchBinds::new(sql),
160        }
161    }
162
163    /// Add a row of bind values
164    pub fn add_row(mut self, values: Vec<Value>) -> Self {
165        self.batch.add_row(values);
166        self
167    }
168
169    /// Add multiple rows at once
170    pub fn add_rows(mut self, rows: Vec<Vec<Value>>) -> Self {
171        for row in rows {
172            self.batch.add_row(row);
173        }
174        self
175    }
176
177    /// Enable batch errors mode
178    pub fn with_batch_errors(mut self) -> Self {
179        self.batch.options.batch_errors = true;
180        self
181    }
182
183    /// Enable array DML row counts
184    pub fn with_row_counts(mut self) -> Self {
185        self.batch.options.array_dml_row_counts = true;
186        self
187    }
188
189    /// Enable auto-commit
190    pub fn with_auto_commit(mut self) -> Self {
191        self.batch.options.auto_commit = true;
192        self
193    }
194
195    /// Build the batch
196    pub fn build(self) -> BatchBinds {
197        self.batch
198    }
199}
200
201/// Result of batch execution
202#[derive(Debug, Clone)]
203pub struct BatchResult {
204    /// Number of rows affected for each statement (if requested)
205    pub row_counts: Option<Vec<u64>>,
206    /// Total rows affected
207    pub total_rows_affected: u64,
208    /// Errors encountered (if batch_errors mode enabled)
209    pub errors: Vec<BatchError>,
210    /// Number of successful executions
211    pub success_count: usize,
212    /// Number of failed executions
213    pub failure_count: usize,
214}
215
216impl BatchResult {
217    /// Create a new empty batch result
218    pub fn new() -> Self {
219        Self {
220            row_counts: None,
221            total_rows_affected: 0,
222            errors: Vec::new(),
223            success_count: 0,
224            failure_count: 0,
225        }
226    }
227
228    /// Create a batch result with row counts
229    pub fn with_row_counts(counts: Vec<u64>) -> Self {
230        let total: u64 = counts.iter().sum();
231        let success_count = counts.len();
232        Self {
233            row_counts: Some(counts),
234            total_rows_affected: total,
235            errors: Vec::new(),
236            success_count,
237            failure_count: 0,
238        }
239    }
240
241    /// Check if all executions succeeded
242    pub fn is_success(&self) -> bool {
243        self.errors.is_empty()
244    }
245
246    /// Check if there were any errors
247    pub fn has_errors(&self) -> bool {
248        !self.errors.is_empty()
249    }
250}
251
252impl Default for BatchResult {
253    fn default() -> Self {
254        Self::new()
255    }
256}
257
258/// An error that occurred during batch execution
259#[derive(Debug, Clone)]
260pub struct BatchError {
261    /// The row index where the error occurred (0-based)
262    pub row_index: usize,
263    /// Oracle error code
264    pub code: u32,
265    /// Error message
266    pub message: String,
267}
268
269impl BatchError {
270    /// Create a new batch error
271    pub fn new(row_index: usize, code: u32, message: impl Into<String>) -> Self {
272        Self {
273            row_index,
274            code,
275            message: message.into(),
276        }
277    }
278}
279
280impl std::fmt::Display for BatchError {
281    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282        write!(
283            f,
284            "Row {}: ORA-{:05}: {}",
285            self.row_index, self.code, self.message
286        )
287    }
288}
289
290impl std::error::Error for BatchError {}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn test_batch_builder() {
298        let batch = BatchBuilder::new("INSERT INTO t (a, b) VALUES (:1, :2)")
299            .add_row(vec![Value::Integer(1), Value::String("a".to_string())])
300            .add_row(vec![Value::Integer(2), Value::String("b".to_string())])
301            .build();
302
303        assert_eq!(batch.row_count(), 2);
304        assert_eq!(batch.column_count(), 2);
305        assert!(!batch.is_empty());
306    }
307
308    #[test]
309    fn test_batch_validation() {
310        let mut batch = BatchBinds::new("INSERT INTO t (a) VALUES (:1)");
311        batch.add_row(vec![Value::Integer(1)]);
312        batch.add_row(vec![Value::Integer(2)]);
313
314        assert!(batch.validate().is_ok());
315    }
316
317    #[test]
318    fn test_batch_validation_empty() {
319        let batch = BatchBinds::new("INSERT INTO t (a) VALUES (:1)");
320        assert!(batch.validate().is_err());
321    }
322
323    #[test]
324    fn test_batch_options() {
325        let opts = BatchOptions::new()
326            .with_batch_errors()
327            .with_row_counts()
328            .with_auto_commit();
329
330        assert!(opts.batch_errors);
331        assert!(opts.array_dml_row_counts);
332        assert!(opts.auto_commit);
333    }
334
335    #[test]
336    fn test_batch_result() {
337        let result = BatchResult::with_row_counts(vec![1, 2, 3]);
338
339        assert_eq!(result.total_rows_affected, 6);
340        assert_eq!(result.success_count, 3);
341        assert!(result.is_success());
342        assert!(!result.has_errors());
343    }
344
345    #[test]
346    fn test_batch_error_display() {
347        let err = BatchError::new(5, 1, "test error");
348        assert_eq!(err.to_string(), "Row 5: ORA-00001: test error");
349    }
350
351    #[test]
352    fn test_add_multiple_rows() {
353        let batch = BatchBuilder::new("INSERT INTO t (a) VALUES (:1)")
354            .add_rows(vec![
355                vec![Value::Integer(1)],
356                vec![Value::Integer(2)],
357                vec![Value::Integer(3)],
358            ])
359            .build();
360
361        assert_eq!(batch.row_count(), 3);
362    }
363}