oracle_rs/
statement.rs

1//! SQL Statement handling
2//!
3//! This module provides types for representing and executing SQL statements,
4//! including support for bind parameters and result set metadata.
5
6use crate::constants::{BindDirection, OracleType};
7use crate::row::Value;
8use crate::types::RefCursor;
9
10/// Statement type determined by parsing the SQL
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum StatementType {
13    /// Unknown or unparsed statement
14    #[default]
15    Unknown,
16    /// SELECT query
17    Query,
18    /// DML: INSERT, UPDATE, DELETE, MERGE
19    Dml,
20    /// DDL: CREATE, ALTER, DROP, etc.
21    Ddl,
22    /// PL/SQL block: BEGIN, DECLARE, CALL
23    PlSql,
24}
25
26/// Metadata for a bind parameter
27#[derive(Debug, Clone)]
28pub struct BindInfo {
29    /// Parameter name (without leading colon)
30    pub name: String,
31    /// Whether this is a RETURNING INTO bind
32    pub is_return_bind: bool,
33    /// Oracle type number
34    pub oracle_type: Option<OracleType>,
35    /// Buffer size
36    pub buffer_size: u32,
37    /// Precision (for NUMBER)
38    pub precision: i16,
39    /// Scale (for NUMBER)
40    pub scale: i16,
41    /// Character set form (1=implicit, 2=nchar)
42    pub csfrm: u8,
43    /// Bind direction
44    pub bind_dir: u8,
45    /// Whether this is an array bind
46    pub is_array: bool,
47    /// Number of array elements
48    pub num_elements: u32,
49}
50
51impl BindInfo {
52    /// Create a new bind parameter with the given name
53    pub fn new(name: impl Into<String>, is_return_bind: bool) -> Self {
54        Self {
55            name: name.into(),
56            is_return_bind,
57            oracle_type: None,
58            buffer_size: 0,
59            precision: 0,
60            scale: 0,
61            csfrm: 0,
62            bind_dir: crate::constants::bind_dir::INPUT,
63            is_array: false,
64            num_elements: 0,
65        }
66    }
67}
68
69/// A bind parameter for PL/SQL execution with direction support
70///
71/// This struct allows specifying IN, OUT, and IN OUT parameters for PL/SQL calls.
72///
73/// # Examples
74///
75/// ```ignore
76/// use oracle_rs::{BindParam, BindDirection, OracleType, Value};
77///
78/// // IN parameter (default)
79/// let in_param = BindParam::input(Value::Integer(42));
80///
81/// // OUT parameter - specify the expected type and size
82/// let out_param = BindParam::output(OracleType::Varchar, 100);
83///
84/// // IN OUT parameter
85/// let inout_param = BindParam::input_output(Value::String("hello".into()), 100);
86///
87/// // Execute PL/SQL
88/// let results = conn.execute_plsql(
89///     "BEGIN :1 := :2 * 2; END;",
90///     &[out_param, in_param]
91/// ).await?;
92/// ```
93#[derive(Debug, Clone)]
94pub struct BindParam {
95    /// The value (None for pure OUT parameters)
96    pub value: Option<Value>,
97    /// Parameter direction
98    pub direction: BindDirection,
99    /// Oracle type (required for OUT parameters)
100    pub oracle_type: OracleType,
101    /// Buffer size for OUT parameters
102    pub buffer_size: u32,
103}
104
105impl BindParam {
106    /// Create an IN (input) parameter from a value
107    pub fn input(value: Value) -> Self {
108        let oracle_type = Self::infer_oracle_type(&value);
109        Self {
110            value: Some(value),
111            direction: BindDirection::Input,
112            oracle_type,
113            buffer_size: 0, // Will be calculated from value
114        }
115    }
116
117    /// Create an OUT (output) parameter with the expected type and size
118    pub fn output(oracle_type: OracleType, buffer_size: u32) -> Self {
119        Self {
120            value: None,
121            direction: BindDirection::Output,
122            oracle_type,
123            buffer_size,
124        }
125    }
126
127    /// Create an OUT parameter for a REF CURSOR
128    pub fn output_cursor() -> Self {
129        Self {
130            value: Some(Value::Cursor(RefCursor::new(0, vec![]))), // Placeholder cursor
131            direction: BindDirection::Output,
132            oracle_type: OracleType::Cursor,
133            buffer_size: 0,
134        }
135    }
136
137    /// Create an OUT parameter for a collection (VARRAY, Nested Table)
138    ///
139    /// The DbObjectType provides element type information for proper decoding.
140    pub fn output_collection(obj_type: &crate::dbobject::DbObjectType) -> Self {
141        use crate::dbobject::DbObject;
142        // Create a placeholder collection with the type info
143        let mut placeholder = DbObject::collection(&obj_type.full_name());
144        placeholder.set("_type_schema", Value::String(obj_type.schema.clone()));
145        placeholder.set("_type_name", Value::String(obj_type.name.clone()));
146        if let Some(elem_type) = obj_type.element_type {
147            placeholder.set("_element_type", Value::Integer(elem_type as i64));
148        }
149        // Store the type OID for bind metadata
150        if let Some(ref oid) = obj_type.oid {
151            placeholder.set("_type_oid", Value::Bytes(oid.clone()));
152        }
153        Self {
154            value: Some(Value::Collection(placeholder)),
155            direction: BindDirection::Output,
156            oracle_type: OracleType::Object,
157            buffer_size: 0,
158        }
159    }
160
161    /// Create an IN parameter for a collection (VARRAY, Nested Table)
162    ///
163    /// The DbObjectType provides element type information for proper encoding,
164    /// and the DbObject contains the actual element values.
165    ///
166    /// # Example
167    /// ```ignore
168    /// let varray_type = conn.get_type("NUMBER_VARRAY").await?;
169    /// let mut coll = DbObject::collection("NUMBER_VARRAY");
170    /// coll.append(Value::Integer(1));
171    /// coll.append(Value::Integer(2));
172    /// let param = BindParam::input_collection(&varray_type, coll);
173    /// ```
174    pub fn input_collection(
175        obj_type: &crate::dbobject::DbObjectType,
176        collection: crate::dbobject::DbObject,
177    ) -> Self {
178        use crate::constants::collection_type;
179        use crate::dbobject::{CollectionType, DbObject};
180        // Create a collection with both elements and type info
181        let mut coll = DbObject::collection(&obj_type.full_name());
182        // Copy elements
183        coll.elements = collection.elements;
184        // Add type metadata
185        coll.set("_type_schema", Value::String(obj_type.schema.clone()));
186        coll.set("_type_name", Value::String(obj_type.name.clone()));
187        if let Some(elem_type) = obj_type.element_type {
188            coll.set("_element_type", Value::Integer(elem_type as i64));
189        }
190        // Store collection type as wire constant (1=index-by, 2=nested, 3=varray)
191        if let Some(coll_type) = obj_type.collection_type {
192            let wire_code = match coll_type {
193                CollectionType::PlsqlIndexTable => collection_type::PLSQL_INDEX_TABLE,
194                CollectionType::NestedTable => collection_type::NESTED_TABLE,
195                CollectionType::Varray => collection_type::VARRAY,
196            };
197            coll.set("_collection_type", Value::Integer(wire_code as i64));
198        }
199        // Store the type OID for TOID construction
200        if let Some(ref oid) = obj_type.oid {
201            coll.set("_type_oid", Value::Bytes(oid.clone()));
202        }
203        Self {
204            value: Some(Value::Collection(coll)),
205            direction: BindDirection::Input,
206            oracle_type: OracleType::Object,
207            buffer_size: 0,
208        }
209    }
210
211    /// Create an IN OUT (input/output) parameter
212    pub fn input_output(value: Value, buffer_size: u32) -> Self {
213        let oracle_type = Self::infer_oracle_type(&value);
214        Self {
215            value: Some(value),
216            direction: BindDirection::InputOutput,
217            oracle_type,
218            buffer_size,
219        }
220    }
221
222    /// Create a placeholder value for OUT parameters based on the Oracle type
223    /// This is used when sending bind metadata to the server
224    pub fn placeholder_value(&self) -> Value {
225        if let Some(ref v) = self.value {
226            return v.clone();
227        }
228
229        // Create an appropriate placeholder based on the oracle type
230        match self.oracle_type {
231            OracleType::Varchar | OracleType::Char | OracleType::Long => {
232                Value::String(String::new())
233            }
234            OracleType::Number | OracleType::BinaryInteger => Value::Integer(0),
235            OracleType::BinaryDouble | OracleType::BinaryFloat => Value::Float(0.0),
236            OracleType::Date => Value::Null,
237            OracleType::Timestamp | OracleType::TimestampTz | OracleType::TimestampLtz => {
238                Value::Null
239            }
240            OracleType::Raw | OracleType::LongRaw => Value::Bytes(Vec::new()),
241            OracleType::Clob | OracleType::Blob => Value::Null,
242            OracleType::Cursor => Value::Cursor(RefCursor::new(0, vec![])),
243            OracleType::Boolean => Value::Boolean(false),
244            _ => Value::Null,
245        }
246    }
247
248    /// Infer the Oracle type from a Value
249    fn infer_oracle_type(value: &Value) -> OracleType {
250        match value {
251            Value::Null => OracleType::Varchar, // Default to VARCHAR for NULL
252            Value::String(_) => OracleType::Varchar,
253            Value::Bytes(_) => OracleType::Raw,
254            Value::Integer(_) => OracleType::Number,
255            Value::Float(_) => OracleType::BinaryDouble,
256            Value::Number(_) => OracleType::Number,
257            Value::Date(_) => OracleType::Date,
258            Value::Timestamp(_) => OracleType::Timestamp,
259            Value::RowId(_) => OracleType::Rowid,
260            Value::Boolean(_) => OracleType::Boolean,
261            Value::Lob(_) => OracleType::Clob, // Default to CLOB
262            Value::Json(_) => OracleType::Json,
263            Value::Vector(_) => OracleType::Vector,
264            Value::Cursor(_) => OracleType::Cursor,
265            Value::Collection(_) => OracleType::Object,
266        }
267    }
268}
269
270impl From<Value> for BindParam {
271    fn from(value: Value) -> Self {
272        BindParam::input(value)
273    }
274}
275
276impl From<i32> for BindParam {
277    fn from(v: i32) -> Self {
278        BindParam::input(Value::Integer(v as i64))
279    }
280}
281
282impl From<i64> for BindParam {
283    fn from(v: i64) -> Self {
284        BindParam::input(Value::Integer(v))
285    }
286}
287
288impl From<f64> for BindParam {
289    fn from(v: f64) -> Self {
290        BindParam::input(Value::Float(v))
291    }
292}
293
294impl From<&str> for BindParam {
295    fn from(v: &str) -> Self {
296        BindParam::input(Value::String(v.to_string()))
297    }
298}
299
300impl From<String> for BindParam {
301    fn from(v: String) -> Self {
302        BindParam::input(Value::String(v))
303    }
304}
305
306impl From<bool> for BindParam {
307    fn from(v: bool) -> Self {
308        BindParam::input(Value::Boolean(v))
309    }
310}
311
312/// Metadata for a column in a result set
313#[derive(Debug, Clone)]
314pub struct ColumnInfo {
315    /// Column name
316    pub name: String,
317    /// Oracle data type
318    pub oracle_type: OracleType,
319    /// Data type size
320    pub data_size: u32,
321    /// Buffer size for fetching
322    pub buffer_size: u32,
323    /// Precision (for NUMBER)
324    pub precision: i16,
325    /// Scale (for NUMBER)
326    pub scale: i16,
327    /// Whether NULL values are allowed
328    pub nullable: bool,
329    /// Character set form
330    pub csfrm: u8,
331    /// Schema name (for object types)
332    pub type_schema: Option<String>,
333    /// Type name (for object types)
334    pub type_name: Option<String>,
335    /// Domain schema (23ai+)
336    pub domain_schema: Option<String>,
337    /// Domain name (23ai+)
338    pub domain_name: Option<String>,
339    /// Is JSON column
340    pub is_json: bool,
341    /// Is OSON format
342    pub is_oson: bool,
343    /// Vector dimensions (23ai+)
344    pub vector_dimensions: Option<u32>,
345    /// Vector format (23ai+)
346    pub vector_format: Option<u8>,
347    /// Element type for collections (VARRAY, Nested Table)
348    pub element_type: Option<OracleType>,
349}
350
351impl ColumnInfo {
352    /// Create a new column with minimal info
353    pub fn new(name: impl Into<String>, oracle_type: OracleType) -> Self {
354        Self {
355            name: name.into(),
356            oracle_type,
357            data_size: 0,
358            buffer_size: 0,
359            precision: 0,
360            scale: 0,
361            nullable: true,
362            csfrm: 0,
363            type_schema: None,
364            type_name: None,
365            domain_schema: None,
366            domain_name: None,
367            is_json: false,
368            is_oson: false,
369            vector_dimensions: None,
370            vector_format: None,
371            element_type: None,
372        }
373    }
374
375    /// Check if this column is a LOB type (CLOB, BLOB, BFILE, JSON, or VECTOR)
376    pub fn is_lob(&self) -> bool {
377        self.oracle_type.is_lob()
378    }
379
380    /// Check if this column requires no prefetch (LOB types)
381    pub fn requires_no_prefetch(&self) -> bool {
382        self.oracle_type.requires_no_prefetch()
383    }
384}
385
386/// A parsed SQL statement ready for execution
387#[derive(Debug, Clone)]
388pub struct Statement {
389    /// The original SQL text
390    sql: String,
391    /// SQL as bytes (for sending to server)
392    sql_bytes: Vec<u8>,
393    /// Statement type
394    statement_type: StatementType,
395    /// Cursor ID assigned by server (0 = not yet assigned)
396    cursor_id: u16,
397    /// List of bind parameters in order of appearance
398    bind_info_list: Vec<BindInfo>,
399    /// Column metadata for queries
400    columns: Vec<ColumnInfo>,
401    /// Whether the statement has been executed
402    executed: bool,
403    /// Whether bind metadata has changed
404    binds_changed: bool,
405    /// Whether column defines are required
406    requires_define: bool,
407    /// Whether prefetch should be disabled (for LOBs)
408    no_prefetch: bool,
409    /// Whether this is a DML RETURNING statement
410    is_returning: bool,
411}
412
413impl Statement {
414    /// Create a new statement from SQL text
415    pub fn new(sql: impl Into<String>) -> Self {
416        let sql = sql.into();
417        let sql_bytes = sql.as_bytes().to_vec();
418
419        let mut stmt = Self {
420            sql,
421            sql_bytes,
422            statement_type: StatementType::Unknown,
423            cursor_id: 0,
424            bind_info_list: Vec::new(),
425            columns: Vec::new(),
426            executed: false,
427            binds_changed: false,
428            requires_define: false,
429            no_prefetch: false,
430            is_returning: false,
431        };
432
433        stmt.parse();
434        stmt
435    }
436
437    /// Get the SQL text
438    pub fn sql(&self) -> &str {
439        &self.sql
440    }
441
442    /// Get the SQL bytes
443    pub fn sql_bytes(&self) -> &[u8] {
444        &self.sql_bytes
445    }
446
447    /// Get the statement type
448    pub fn statement_type(&self) -> StatementType {
449        self.statement_type
450    }
451
452    /// Check if this is a query (SELECT)
453    pub fn is_query(&self) -> bool {
454        self.statement_type == StatementType::Query
455    }
456
457    /// Check if this is a DML statement
458    pub fn is_dml(&self) -> bool {
459        self.statement_type == StatementType::Dml
460    }
461
462    /// Check if this is a DDL statement
463    pub fn is_ddl(&self) -> bool {
464        self.statement_type == StatementType::Ddl
465    }
466
467    /// Check if this is a PL/SQL block
468    pub fn is_plsql(&self) -> bool {
469        self.statement_type == StatementType::PlSql
470    }
471
472    /// Check if this is a RETURNING statement
473    pub fn is_returning(&self) -> bool {
474        self.is_returning
475    }
476
477    /// Get the cursor ID
478    pub fn cursor_id(&self) -> u16 {
479        self.cursor_id
480    }
481
482    /// Set the cursor ID
483    pub fn set_cursor_id(&mut self, id: u16) {
484        self.cursor_id = id;
485    }
486
487    /// Get the bind parameters
488    pub fn bind_info(&self) -> &[BindInfo] {
489        &self.bind_info_list
490    }
491
492    /// Get the column metadata
493    pub fn columns(&self) -> &[ColumnInfo] {
494        &self.columns
495    }
496
497    /// Set column metadata (from server describe)
498    pub fn set_columns(&mut self, columns: Vec<ColumnInfo>) {
499        self.columns = columns;
500    }
501
502    /// Get the number of columns
503    pub fn column_count(&self) -> usize {
504        self.columns.len()
505    }
506
507    /// Check if the statement has been executed
508    pub fn executed(&self) -> bool {
509        self.executed
510    }
511
512    /// Mark the statement as executed
513    pub fn set_executed(&mut self, executed: bool) {
514        self.executed = executed;
515    }
516
517    /// Check if binds have changed
518    pub fn binds_changed(&self) -> bool {
519        self.binds_changed
520    }
521
522    /// Set binds changed flag
523    pub fn set_binds_changed(&mut self, changed: bool) {
524        self.binds_changed = changed;
525    }
526
527    /// Check if column define is required
528    pub fn requires_define(&self) -> bool {
529        self.requires_define
530    }
531
532    /// Set requires define flag
533    pub fn set_requires_define(&mut self, required: bool) {
534        self.requires_define = required;
535    }
536
537    /// Check if prefetch should be disabled
538    pub fn no_prefetch(&self) -> bool {
539        self.no_prefetch
540    }
541
542    /// Set no prefetch flag
543    pub fn set_no_prefetch(&mut self, no_prefetch: bool) {
544        self.no_prefetch = no_prefetch;
545    }
546
547    /// Set the statement type (useful for scroll operations that reuse cursors)
548    pub fn set_statement_type(&mut self, stmt_type: StatementType) {
549        self.statement_type = stmt_type;
550    }
551
552    /// Parse the SQL to determine statement type and extract bind names
553    fn parse(&mut self) {
554        let sql_upper = self.sql.to_uppercase();
555        let trimmed = sql_upper.trim_start();
556
557        // Determine statement type from first keyword
558        if let Some(first_word) = trimmed.split_whitespace().next() {
559            self.statement_type = match first_word {
560                "SELECT" | "WITH" => StatementType::Query,
561                "INSERT" | "UPDATE" | "DELETE" | "MERGE" => StatementType::Dml,
562                "CREATE" | "ALTER" | "DROP" | "GRANT" | "REVOKE" | "ANALYZE" | "AUDIT"
563                | "COMMENT" | "TRUNCATE" => StatementType::Ddl,
564                "DECLARE" | "BEGIN" | "CALL" => StatementType::PlSql,
565                _ => StatementType::Unknown,
566            };
567        }
568
569        // Don't parse binds for DDL
570        if self.statement_type == StatementType::Ddl {
571            return;
572        }
573
574        // Parse bind variables and check for RETURNING INTO
575        self.parse_bind_variables();
576    }
577
578    /// Parse bind variables from SQL text
579    fn parse_bind_variables(&mut self) {
580        let sql = &self.sql;
581        let sql_upper = sql.to_uppercase();
582        let chars: Vec<char> = sql.chars().collect();
583        let chars_upper: Vec<char> = sql_upper.chars().collect();
584        let len = chars.len();
585
586        let mut i = 0;
587        let mut in_string = false;
588        let mut in_comment = false;
589        let mut in_line_comment = false;
590        let mut returning_found = false;
591        let mut into_found = false;
592
593        while i < len {
594            let ch = chars[i];
595
596            // Handle string literals
597            if ch == '\'' && !in_comment && !in_line_comment {
598                in_string = !in_string;
599                i += 1;
600                continue;
601            }
602
603            if in_string {
604                i += 1;
605                continue;
606            }
607
608            // Handle line comments (--)
609            if ch == '-' && i + 1 < len && chars[i + 1] == '-' {
610                in_line_comment = true;
611                i += 2;
612                continue;
613            }
614
615            if in_line_comment {
616                if ch == '\n' {
617                    in_line_comment = false;
618                }
619                i += 1;
620                continue;
621            }
622
623            // Handle block comments (/* */)
624            if ch == '/' && i + 1 < len && chars[i + 1] == '*' {
625                in_comment = true;
626                i += 2;
627                continue;
628            }
629
630            if in_comment {
631                if ch == '*' && i + 1 < len && chars[i + 1] == '/' {
632                    in_comment = false;
633                    i += 2;
634                } else {
635                    i += 1;
636                }
637                continue;
638            }
639
640            // Check for RETURNING keyword (for DML)
641            if self.statement_type == StatementType::Dml && !returning_found {
642                if self.match_keyword(&chars_upper, i, "RETURNING") {
643                    returning_found = true;
644                    i += 9;
645                    continue;
646                }
647            }
648
649            // Check for INTO keyword (after RETURNING)
650            if returning_found && !into_found {
651                if self.match_keyword(&chars_upper, i, "INTO") {
652                    into_found = true;
653                    self.is_returning = true;
654                    i += 4;
655                    continue;
656                }
657            }
658
659            // Parse bind variable
660            if ch == ':' && i + 1 < len {
661                let bind_name = self.extract_bind_name(&chars, i + 1);
662                if !bind_name.is_empty() {
663                    // Check if bind already exists for PL/SQL (allow duplicates otherwise)
664                    let should_add = if self.statement_type == StatementType::PlSql {
665                        !self.bind_info_list.iter().any(|b| b.name == bind_name)
666                    } else {
667                        true
668                    };
669
670                    if should_add {
671                        self.bind_info_list
672                            .push(BindInfo::new(bind_name.clone(), into_found));
673                    }
674                    i += 1 + bind_name.len();
675                    continue;
676                }
677            }
678
679            i += 1;
680        }
681        // Note: bind_info_list is in order of appearance in SQL, not numerical order.
682        // Oracle expects params in this order, so we don't sort.
683    }
684
685    /// Check if keyword matches at position
686    fn match_keyword(&self, chars: &[char], pos: usize, keyword: &str) -> bool {
687        let keyword_chars: Vec<char> = keyword.chars().collect();
688        let len = chars.len();
689
690        // Check bounds
691        if pos + keyword.len() > len {
692            return false;
693        }
694
695        // Check preceding character is not alphanumeric
696        if pos > 0 && chars[pos - 1].is_alphanumeric() {
697            return false;
698        }
699
700        // Check keyword matches
701        for (i, kc) in keyword_chars.iter().enumerate() {
702            if chars[pos + i] != *kc {
703                return false;
704            }
705        }
706
707        // Check following character is not alphanumeric
708        let end_pos = pos + keyword.len();
709        if end_pos < len && chars[end_pos].is_alphanumeric() {
710            return false;
711        }
712
713        true
714    }
715
716    /// Extract bind variable name starting at position
717    fn extract_bind_name(&self, chars: &[char], start: usize) -> String {
718        let len = chars.len();
719
720        // Skip leading whitespace
721        let mut i = start;
722        while i < len && chars[i].is_whitespace() {
723            i += 1;
724        }
725
726        if i >= len {
727            return String::new();
728        }
729
730        let first_char = chars[i];
731
732        // Quoted bind name
733        if first_char == '"' {
734            i += 1;
735            let name_start = i;
736            while i < len && chars[i] != '"' {
737                i += 1;
738            }
739            if i > name_start {
740                return chars[name_start..i].iter().collect();
741            }
742            return String::new();
743        }
744
745        // Numeric bind (positional)
746        if first_char.is_ascii_digit() {
747            let name_start = i;
748            while i < len && chars[i].is_ascii_digit() {
749                i += 1;
750            }
751            return chars[name_start..i].iter().collect();
752        }
753
754        // Regular bind name (must start with letter)
755        if !first_char.is_alphabetic() {
756            return String::new();
757        }
758
759        let name_start = i;
760        while i < len {
761            let ch = chars[i];
762            if ch.is_alphanumeric() || ch == '_' || ch == '$' || ch == '#' {
763                i += 1;
764            } else {
765                break;
766            }
767        }
768
769        // Convert to uppercase for non-quoted names
770        chars[name_start..i]
771            .iter()
772            .collect::<String>()
773            .to_uppercase()
774    }
775
776    /// Clear statement state for re-execution
777    pub fn clear(&mut self) {
778        self.cursor_id = 0;
779        self.columns.clear();
780        self.executed = false;
781        self.binds_changed = false;
782        self.requires_define = false;
783        self.no_prefetch = false;
784    }
785
786    /// Clone statement for cache reuse, preserving cursor_id and metadata
787    ///
788    /// This creates a copy of the statement that can be executed with new
789    /// bind values while reusing the server-side cursor. The cursor_id,
790    /// column metadata, and bind info are preserved.
791    pub fn clone_for_reuse(&self) -> Self {
792        Self {
793            sql: self.sql.clone(),
794            sql_bytes: self.sql_bytes.clone(),
795            statement_type: self.statement_type,
796            cursor_id: self.cursor_id, // Preserve cursor!
797            bind_info_list: self.bind_info_list.clone(),
798            columns: self.columns.clone(),
799            executed: self.executed,
800            binds_changed: false, // Reset for new execution
801            requires_define: false,
802            no_prefetch: self.no_prefetch,
803            is_returning: self.is_returning,
804        }
805    }
806}
807
808#[cfg(test)]
809mod tests {
810    use super::*;
811
812    #[test]
813    fn test_statement_type_detection() {
814        assert_eq!(
815            Statement::new("SELECT * FROM dual").statement_type(),
816            StatementType::Query
817        );
818        assert_eq!(
819            Statement::new("INSERT INTO t VALUES (1)").statement_type(),
820            StatementType::Dml
821        );
822        assert_eq!(
823            Statement::new("UPDATE t SET x = 1").statement_type(),
824            StatementType::Dml
825        );
826        assert_eq!(
827            Statement::new("DELETE FROM t").statement_type(),
828            StatementType::Dml
829        );
830        assert_eq!(
831            Statement::new("CREATE TABLE t (x NUMBER)").statement_type(),
832            StatementType::Ddl
833        );
834        assert_eq!(
835            Statement::new("BEGIN NULL; END;").statement_type(),
836            StatementType::PlSql
837        );
838        assert_eq!(
839            Statement::new("DECLARE x NUMBER; BEGIN NULL; END;").statement_type(),
840            StatementType::PlSql
841        );
842    }
843
844    #[test]
845    fn test_bind_variable_extraction() {
846        let stmt = Statement::new("SELECT * FROM t WHERE x = :x AND y = :y");
847        assert_eq!(stmt.bind_info().len(), 2);
848        assert_eq!(stmt.bind_info()[0].name, "X");
849        assert_eq!(stmt.bind_info()[1].name, "Y");
850    }
851
852    #[test]
853    fn test_numeric_bind_variables() {
854        let stmt = Statement::new("SELECT * FROM t WHERE x = :1 AND y = :2");
855        assert_eq!(stmt.bind_info().len(), 2);
856        assert_eq!(stmt.bind_info()[0].name, "1");
857        assert_eq!(stmt.bind_info()[1].name, "2");
858    }
859
860    #[test]
861    fn test_duplicate_binds_plsql() {
862        // PL/SQL should deduplicate bind names
863        let stmt = Statement::new("BEGIN :x := :x + 1; END;");
864        assert_eq!(stmt.bind_info().len(), 1);
865        assert_eq!(stmt.bind_info()[0].name, "X");
866    }
867
868    #[test]
869    fn test_duplicate_binds_sql() {
870        // SQL allows duplicate bind positions
871        let stmt = Statement::new("SELECT * FROM t WHERE x = :x OR y = :x");
872        assert_eq!(stmt.bind_info().len(), 2);
873    }
874
875    #[test]
876    fn test_returning_into() {
877        let stmt = Statement::new("INSERT INTO t (x) VALUES (:val) RETURNING id INTO :id");
878        assert!(stmt.is_returning());
879        assert_eq!(stmt.bind_info().len(), 2);
880        assert!(!stmt.bind_info()[0].is_return_bind); // :val
881        assert!(stmt.bind_info()[1].is_return_bind); // :id
882    }
883
884    #[test]
885    fn test_binds_in_comments_ignored() {
886        let stmt = Statement::new("SELECT * FROM t WHERE x = :x -- AND y = :y");
887        assert_eq!(stmt.bind_info().len(), 1);
888        assert_eq!(stmt.bind_info()[0].name, "X");
889    }
890
891    #[test]
892    fn test_binds_in_strings_ignored() {
893        let stmt = Statement::new("SELECT * FROM t WHERE x = ':not_a_bind' AND y = :y");
894        assert_eq!(stmt.bind_info().len(), 1);
895        assert_eq!(stmt.bind_info()[0].name, "Y");
896    }
897
898    #[test]
899    fn test_with_query() {
900        let stmt = Statement::new("WITH cte AS (SELECT 1 x FROM dual) SELECT * FROM cte");
901        assert!(stmt.is_query());
902    }
903
904    #[test]
905    fn test_quoted_bind_name() {
906        let stmt = Statement::new("SELECT * FROM t WHERE x = :\"MyBind\"");
907        assert_eq!(stmt.bind_info().len(), 1);
908        assert_eq!(stmt.bind_info()[0].name, "MyBind");
909    }
910
911    #[test]
912    fn test_case_insensitive_keywords() {
913        assert!(Statement::new("select * from dual").is_query());
914        assert!(Statement::new("Select * From dual").is_query());
915        assert!(Statement::new("INSERT into t values (1)").is_dml());
916    }
917}