executemany method

Future executemany(
  1. List<List> parameters, {
  2. bool batchErrors = false,
  3. bool arrayDmlRowCounts = false,
})

Execute statement multiple times with different bind values

Similar to python-oracledb's executemany(). Executes a SQL statement multiple times with different sets of parameters using Oracle array DML.

This implementation uses true Oracle array DML with a single round trip to the database, providing 10-100x better performance than loop-based execution for large batches.

Parameters:

  • parameters: List of parameter sets, where each set is a List of values
  • batchErrors: If true, collect errors and continue (default: false)
  • arrayDmlRowCounts: If true, return row counts for each execution (default: false)

Returns the total number of rows affected, or a list of row counts if arrayDmlRowCounts is true

Example:

final stmt = await conn.execute('INSERT INTO users (name, age) VALUES (?, ?)');
final rowCounts = await stmt.executemany([
  ['Alice', 30],
  ['Bob', 25],
  ['Charlie', 35],
], arrayDmlRowCounts: true);
print('Inserted ${rowCounts.fold(0, (a, b) => a + b)} rows');

Implementation

Future<dynamic> executemany(
  List<List<dynamic>> parameters, {
  bool batchErrors = false,
  bool arrayDmlRowCounts = false,
}) async {
  if (_disposed) {
    throw OracleStatementException('Statement has been closed', sql: _sql);
  }

  if (parameters.isEmpty) {
    return arrayDmlRowCounts ? <int>[] : 0;
  }

  // Validate that all parameter sets have the same length
  final expectedLength = parameters.first.length;
  for (var i = 1; i < parameters.length; i++) {
    if (parameters[i].length != expectedLength) {
      throw OracleStatementException(
        'All parameter sets must have the same length. '
        'Expected $expectedLength parameters, but parameter set $i has ${parameters[i].length}',
        sql: _sql,
      );
    }
  }

  final numRows = parameters.length;
  final numParams = expectedLength;
  final arrayVars = <OracleVariable>[];

  try {
    // Note: Batch error mode is handled internally by ODPI-C
    // The batchErrors parameter controls whether to collect errors

    // Create array variables for each parameter position
    for (var paramPos = 0; paramPos < numParams; paramPos++) {
      final position = paramPos + 1; // 1-based

      // Determine type from first non-null value in this position
      int oracleType;
      int nativeType;
      int size = 0;

      dynamic sampleValue;
      for (var rowIdx = 0; rowIdx < numRows; rowIdx++) {
        final value = parameters[rowIdx][paramPos];
        if (value != null) {
          sampleValue = value;
          break;
        }
      }

      // Determine types based on sample value
      if (sampleValue == null) {
        // All nulls - default to string
        oracleType = DPI_ORACLE_TYPE_VARCHAR;
        nativeType = DPI_NATIVE_TYPE_BYTES;
        size = 1;
      } else if (sampleValue is String) {
        oracleType = DPI_ORACLE_TYPE_VARCHAR;
        nativeType = DPI_NATIVE_TYPE_BYTES;
        // Find max string length in this column (in bytes for UTF-8)
        size = 1;
        for (var rowIdx = 0; rowIdx < numRows; rowIdx++) {
          final value = parameters[rowIdx][paramPos];
          if (value is String && value.isNotEmpty) {
            final byteLength = StringUtils.getUtf8ByteLength(value);
            if (byteLength > size) {
              size = byteLength;
            }
          }
        }
      } else if (sampleValue is int) {
        oracleType = DPI_ORACLE_TYPE_NUMBER;
        nativeType = DPI_NATIVE_TYPE_INT64;
      } else if (sampleValue is double) {
        oracleType = DPI_ORACLE_TYPE_NUMBER;
        nativeType = DPI_NATIVE_TYPE_DOUBLE;
      } else if (sampleValue is DateTime) {
        oracleType = DPI_ORACLE_TYPE_TIMESTAMP;
        nativeType = DPI_NATIVE_TYPE_TIMESTAMP;
      } else if (sampleValue is Duration) {
        oracleType = DPI_ORACLE_TYPE_INTERVAL_DS;
        nativeType = DPI_NATIVE_TYPE_INTERVAL_DS;
      } else if (sampleValue is bool) {
        oracleType = DPI_ORACLE_TYPE_BOOLEAN;
        nativeType = DPI_NATIVE_TYPE_BOOLEAN;
      } else if (sampleValue is Map && sampleValue.containsKey('years') && sampleValue.containsKey('months')) {
        // INTERVAL YEAR TO MONTH
        oracleType = DPI_ORACLE_TYPE_INTERVAL_YM;
        nativeType = DPI_NATIVE_TYPE_INTERVAL_YM;
      } else if (sampleValue is OracleLob) {
        // LOB type
        if (sampleValue.lobType == LobType.clob) {
          oracleType = DPI_ORACLE_TYPE_CLOB;
          nativeType = DPI_NATIVE_TYPE_LOB;
        } else if (sampleValue.lobType == LobType.blob) {
          oracleType = DPI_ORACLE_TYPE_BLOB;
          nativeType = DPI_NATIVE_TYPE_LOB;
        } else {
          oracleType = DPI_ORACLE_TYPE_NCLOB;
          nativeType = DPI_NATIVE_TYPE_LOB;
        }
      } else {
        throw OracleStatementException(
          'Unsupported parameter type at position $position: ${sampleValue.runtimeType}',
          sql: _sql,
        );
      }

      // Create array variable
      final arrayVar = await OracleVariable.create(
        dpiOracle: _dpiOracle,
        connection: _connection,
        context: _context,
        oracleType: oracleType,
        nativeType: nativeType,
        size: size,
        arraySize: numRows,
        direction: BindDirection.input,
      );

      arrayVars.add(arrayVar);

      // Set values for all rows in this array
      for (var rowIdx = 0; rowIdx < numRows; rowIdx++) {
        final value = parameters[rowIdx][paramPos];
        arrayVar.setArrayValue(rowIdx, value);
      }

      // Bind the array variable to the statement
      final bindResult = _dpiOracle.dpiStmt_bindByPos(
        _statementPtr.value,
        position,
        arrayVar.varPointer,
      );

      if (bindResult == DPI_FAILURE) {
        throw OracleStatementException(
          'Failed to bind array variable at position $position',
          sql: _sql,
        );
      }
    }

    // Execute all rows in a single call (true array DML)
    // Set execution mode based on options
    int execMode = DPI_MODE_EXEC_DEFAULT;
    if (arrayDmlRowCounts) {
      execMode |= DPI_MODE_EXEC_ARRAY_DML_ROWCOUNTS;
    }
    if (batchErrors) {
      execMode |= DPI_MODE_EXEC_BATCH_ERRORS;
    }

    final execResult = _dpiOracle.dpiStmt_executeMany(
      _statementPtr.value,
      execMode,
      numRows,
    );

    if (execResult == DPI_FAILURE && !batchErrors) {
      final errorInfo = _memoryManager.allocate<dpiErrorInfo>(sizeOf<dpiErrorInfo>());
      _dpiOracle.dpiContext_getError(_context, errorInfo);
      final errorMsg = StringUtils.fromNativeUtf8(errorInfo.ref.message.cast<Char>());
      final errorCode = errorInfo.ref.code;

      throw OracleStatementException(
        'Failed to execute batch operation',
        errorCode: errorCode,
        errorMessage: errorMsg,
        sql: _sql,
      );
    }

    _executed = true;

    // Get row counts if requested
    if (arrayDmlRowCounts) {
      final numRowCounts = _memoryManager.allocate<Uint32>(sizeOf<Uint32>());
      final rowCountsPtr = _memoryManager.allocate<Pointer<Uint64>>(sizeOf<Pointer<Uint64>>());

      final rowCountResult = _dpiOracle.dpiStmt_getRowCounts(
        _statementPtr.value,
        numRowCounts,
        rowCountsPtr,
      );

      if (rowCountResult == DPI_FAILURE) {
        throw OracleStatementException(
          'Failed to get row counts',
          sql: _sql,
        );
      }

      final rowCounts = <int>[];
      final count = numRowCounts.value;
      for (var i = 0; i < count; i++) {
        final rowCountValue = (rowCountsPtr.value + i).value;
        rowCounts.add(rowCountValue);
      }

      return rowCounts;
    } else {
      // Get total row count
      final rowCount = _memoryManager.allocate<Uint64>(sizeOf<Uint64>());
      _dpiOracle.dpiStmt_getRowCount(_statementPtr.value, rowCount);
      return rowCount.value;
    }
  } finally {
    // Dispose of array variables
    for (final arrayVar in arrayVars) {
      arrayVar.dispose();
    }
  }
}