|
21 | 21 |
|
22 | 22 | logger = get_logger() |
23 | 23 |
|
24 | | - |
25 | 24 | class Cursor: |
26 | 25 | """ |
27 | 26 | Represents a database cursor, which is used to manage the context of a fetch operation. |
@@ -631,37 +630,103 @@ def execute( |
631 | 630 | # Initialize description after execution |
632 | 631 | self._initialize_description() |
633 | 632 |
|
| 633 | + @staticmethod |
| 634 | + def _select_best_sample_value(column): |
| 635 | + """ |
| 636 | + Selects the most representative non-null value from a column for type inference. |
| 637 | +
|
| 638 | + This is used during executemany() to infer SQL/C types based on actual data, |
| 639 | + preferring a non-null value that is not the first row to avoid bias from placeholder defaults. |
| 640 | +
|
| 641 | + Args: |
| 642 | + column: List of values in the column. |
| 643 | + """ |
| 644 | + non_nulls = [v for v in column if v is not None] |
| 645 | + if not non_nulls: |
| 646 | + return None |
| 647 | + if all(isinstance(v, int) for v in non_nulls): |
| 648 | + # Pick the value with the widest range (min/max) |
| 649 | + return max(non_nulls, key=lambda v: abs(v)) |
| 650 | + if all(isinstance(v, float) for v in non_nulls): |
| 651 | + return 0.0 |
| 652 | + if all(isinstance(v, decimal.Decimal) for v in non_nulls): |
| 653 | + return max(non_nulls, key=lambda d: len(d.as_tuple().digits)) |
| 654 | + if all(isinstance(v, str) for v in non_nulls): |
| 655 | + return max(non_nulls, key=lambda s: len(str(s))) |
| 656 | + if all(isinstance(v, datetime.datetime) for v in non_nulls): |
| 657 | + return datetime.datetime.now() |
| 658 | + if all(isinstance(v, datetime.date) for v in non_nulls): |
| 659 | + return datetime.date.today() |
| 660 | + return non_nulls[0] # fallback |
| 661 | + |
| 662 | + def _transpose_rowwise_to_columnwise(self, seq_of_parameters: list) -> list: |
| 663 | + """ |
| 664 | + Convert list of rows (row-wise) into list of columns (column-wise), |
| 665 | + for array binding via ODBC. |
| 666 | + Args: |
| 667 | + seq_of_parameters: Sequence of sequences or mappings of parameters. |
| 668 | + """ |
| 669 | + if not seq_of_parameters: |
| 670 | + return [] |
| 671 | + |
| 672 | + num_params = len(seq_of_parameters[0]) |
| 673 | + columnwise = [[] for _ in range(num_params)] |
| 674 | + for row in seq_of_parameters: |
| 675 | + if len(row) != num_params: |
| 676 | + raise ValueError("Inconsistent parameter row size in executemany()") |
| 677 | + for i, val in enumerate(row): |
| 678 | + columnwise[i].append(val) |
| 679 | + return columnwise |
| 680 | + |
634 | 681 | def executemany(self, operation: str, seq_of_parameters: list) -> None: |
635 | 682 | """ |
636 | 683 | Prepare a database operation and execute it against all parameter sequences. |
637 | | -
|
| 684 | + This version uses column-wise parameter binding and a single batched SQLExecute(). |
638 | 685 | Args: |
639 | 686 | operation: SQL query or command. |
640 | 687 | seq_of_parameters: Sequence of sequences or mappings of parameters. |
641 | 688 |
|
642 | 689 | Raises: |
643 | 690 | Error: If the operation fails. |
644 | 691 | """ |
645 | | - self._check_closed() # Check if the cursor is closed |
646 | | - |
| 692 | + self._check_closed() |
647 | 693 | self._reset_cursor() |
648 | 694 |
|
649 | | - first_execution = True |
650 | | - total_rowcount = 0 |
651 | | - for parameters in seq_of_parameters: |
652 | | - parameters = list(parameters) |
653 | | - if ENABLE_LOGGING: |
654 | | - logger.info("Executing query with parameters: %s", parameters) |
655 | | - prepare_stmt = first_execution |
656 | | - first_execution = False |
657 | | - self.execute( |
658 | | - operation, parameters, use_prepare=prepare_stmt, reset_cursor=False |
| 695 | + if not seq_of_parameters: |
| 696 | + self.rowcount = 0 |
| 697 | + return |
| 698 | + |
| 699 | + param_info = ddbc_bindings.ParamInfo |
| 700 | + param_count = len(seq_of_parameters[0]) |
| 701 | + parameters_type = [] |
| 702 | + |
| 703 | + for col_index in range(param_count): |
| 704 | + column = [row[col_index] for row in seq_of_parameters] |
| 705 | + sample_value = self._select_best_sample_value(column) |
| 706 | + dummy_row = list(seq_of_parameters[0]) |
| 707 | + parameters_type.append( |
| 708 | + self._create_parameter_types_list(sample_value, param_info, dummy_row, col_index) |
659 | 709 | ) |
660 | | - if self.rowcount != -1: |
661 | | - total_rowcount += self.rowcount |
662 | | - else: |
663 | | - total_rowcount = -1 |
664 | | - self.rowcount = total_rowcount |
| 710 | + |
| 711 | + columnwise_params = self._transpose_rowwise_to_columnwise(seq_of_parameters) |
| 712 | + if ENABLE_LOGGING: |
| 713 | + logger.info("Executing batch query with %d parameter sets:\n%s", |
| 714 | + len(seq_of_parameters),"\n".join(f" {i+1}: {tuple(p) if isinstance(p, (list, tuple)) else p}" for i, p in enumerate(seq_of_parameters)) |
| 715 | + ) |
| 716 | + |
| 717 | + # Execute batched statement |
| 718 | + ret = ddbc_bindings.SQLExecuteMany( |
| 719 | + self.hstmt, |
| 720 | + operation, |
| 721 | + columnwise_params, |
| 722 | + parameters_type, |
| 723 | + len(seq_of_parameters) |
| 724 | + ) |
| 725 | + check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret) |
| 726 | + |
| 727 | + self.rowcount = ddbc_bindings.DDBCSQLRowCount(self.hstmt) |
| 728 | + self.last_executed_stmt = operation |
| 729 | + self._initialize_description() |
665 | 730 |
|
666 | 731 | def fetchone(self) -> Union[None, Row]: |
667 | 732 | """ |
|
0 commit comments