I would like to use cx_Oracle with in and out bind variables with named placeholders with Cursor.exeutemany. After a lot of browsing and cul-de-sac-s I could not find a proper way.
This is what I could figure out:
I have created a pretty simple table with one column:
CREATE TABLE mytable (kex VARCHAR2(20 BYTE) NOT NULL ENABLE)
The INSERT also simple. It requires two named placeholders: one for input only (called 'kex') and an input-output ('rid') bind variable:
INSERT INTO mytable VALUES (:kex || :rid) RETURNING rowid INTO :rid
Then I created the bind array with 3 bind-sets:
N = 3
bind = [dict(kex = chr(65+i), rid = chr(75+i)) for i in range(N)]
print('Before set', bind)
Output is:
Before set [{'kex': 'A', 'rid': 'K'}, {'kex': 'B', 'rid': 'L'}, {'kex': 'C', 'rid': 'M'}]
Then I modified the last bind-set's rid value to contain a vector of variables:
conn = cx_Oracle.connect(mydbconn)
curs = conn.cursor()
# Define the bind variable array
var = curs.var(str, size = 50, arraysize = N)
bind[-1]['rid'] = var # Why the final bind-set has to be modified?
# Set the input values in the bind variable array
for i in range(N): var.setvalue(i, chr(97+i)) # To-be commented!!!
print('After set', bind)
Output:
After set [{'kex': 'A', 'rid': 'K'}, {'kex': 'B', 'rid': 'L'}, {'kex': 'C', 'rid': <cx_Oracle.Var of type DB_TYPE_VARCHAR with value ['a', 'b', 'c']>}]
Then executemany the whole bind-set:
sql_txt = "INSERT INTO mytable VALUES (:kex || :rid) RETURNING rowid INTO :rid"
rv = curs.executemany(sql_txt, bind, batcherrors = True
, arraydmlrowcounts = True)
for i in curs.getbatcherrors(): print('Batch err:', i)
print('DML row count:', [i for i in curs.getarraydmlrowcounts()])
print(bind)
print([var.getvalue(i) for i in range(N)])
Output:
DML row count: [1, 1, 1]
[{'kex': 'A', 'rid': 'K'}, {'kex': 'B', 'rid': 'L'}, {'kex': 'C', 'rid': <cx_Oracle.Var of type DB_TYPE_VARCHAR with value ['a', 'b', 'c']>}]
['a', 'b', 'c']
Finally the inserted content is printed:
for i in curs.execute(f"SELECT t.*, rowid FROM mytable t"): print('Ret', i)
Output:
Ret ('Aa', 'ABf0HwAMlAAPmOLAAA')
Ret ('Bb', 'ABf0HwAMlAAPmOLAAB')
Ret ('Cc', 'ABf0HwAMlAAPmOLAAC')
If I simply comment the var.setvalue(i, ...) line from the above code, then I get back the output rowids, but it seems the :rid as an input bind value become NULL somehow.
Complete output without setvalue:
Before set [{'kex': 'A', 'rid': 'K'}, {'kex': 'B', 'rid': 'L'}, {'kex': 'C', 'rid': 'M'}]
After set [{'kex': 'A', 'rid': 'K'}, {'kex': 'B', 'rid': 'L'}, {'kex': 'C', 'rid': <cx_Oracle.Var of type DB_TYPE_VARCHAR with value [None, None, None]>}]
DML row count: [1, 1, 1]
[{'kex': 'A', 'rid': 'K'}, {'kex': 'B', 'rid': 'L'}, {'kex': 'C', 'rid': <cx_Oracle.Var of type DB_TYPE_VARCHAR with value [['ABf0HxAMlAAPmOOAAA'], ['ABf0HxAMlAAPmOOAAB'], ['ABf0HxAMlAAPmOOAAC']]>}]
[['ABf0HxAMlAAPmOOAAA'], ['ABf0HxAMlAAPmOOAAB'], ['ABf0HxAMlAAPmOOAAC']]
Ret ('A', 'ABf0HxAMlAAPmOOAAA')
Ret ('B', 'ABf0HxAMlAAPmOOAAB')
Ret ('C', 'ABf0HxAMlAAPmOOAAC')
So, if setvariable is used, then rid behaves as simple input bind variable (but setvariable overrides the original values of rid). If setvariable is removed, then rid behaves as an output variable. But it seems it cannot work as input-output bind variable.
UPDATE
In Perl I have tried the bind_param_inout_array defined in DBD::Oracle. It can use the ? and :1 bind notation and it works perfectly with in-out variables! I tried with INSERT INTO mytable VALUES (:1 || :2) RETURNING rowid INTO :2. In this case for :1 a vector must be applied using bind_param_array and for :2 also a vector must be applied using bind_param_inout_array.
I also tried to do similar things in Python. But even is I use
val = curs.var(str, size=50)
val.setvalue(0, 'a') # Maybe commented
execute('INSERT INTO mytable VALUES (:1 || :2) RETURNING rowid INTO :2'
, {'1': 'A', '2': val})
it does work only in one direction! If setvalue is applied for Cursor.Val object, then it is used as an output bind variable. If setvalue is not used, then it is an input bind variable (using NULL as input value). It seems Oracle passes and and retrieves values through this variable, but Python is not ready to handle it for some reason.
Does anyone know the solution?
UPDATE2
I add the simplistic test Perl script (Perl: v5.26.2, DBI: 1.641, I have tried Oracle 12c client vs 12c server, 12c client vs 19 server, 19 client vs 19 server). Here in the INSERT the input and output bind variable both goes through :2. The only trick is that an array has to be bound to each bind variable! It does not work with named placeholders, but it does work with numbered placeholders!
use strict; use warnings;
use DBI qw(:sql_types); #use DBD::Oracle qw(:ora_types);
use Data::Dumper;
$Data::Dumper::Indent = 0;
my ($srv, $usr, $pwd) = @ARGV;
my $dbh = DBI->connect("dbi:Oracle:$srv", $usr, $pwd, {AutoCommit=>0, RaiseError=>0});
my $table = 'mytable';
{ my $sql = "CREATE TABLE $table (kex VARCHAR2(20 BYTE) NOT NULL ENABLE)";
my $sth = $dbh->prepare($sql) or warn $dbh->errstr;
my $rv = $sth->execute or warn $sth->errstr; }
my $sql = "INSERT INTO $table VALUES (:1 || :2) RETURNING rowid INTO :2";
my $sth = $dbh->prepare($sql) or die $dbh->errstr;
my @bind = ( ['A', 'B', 'C'], ['K', 'L', 'M'],);
$sth->bind_param_array(1, $bind[0]);
$sth->bind_param_inout_array(2, $bind[1], 200); #, {ora_type => ORA_VARCHAR2});
print "Before exec ", Dumper(\@bind), "\n";
my @rv = $sth->execute_array({}) or die $sth->errstr;
print "returned (tuple, rows) = @rv\n";
print "After exec ", Dumper(\@bind), "\n";
$sth = $dbh->prepare("SELECT t.*, rowid FROM $table t") or die $sth->errstr;
my $rv = $sth->execute() or die $sth->errstr;
$rv = $sth->fetchall_arrayref;
print "Returned data", Dumper($rv), "\n";
$dbh->rollback;
Called with SRV USER PASSWD command line arguments, the result is:
Before exec $VAR1 = [['A','B','C'],['K','L','M']];
returned (tuple, rows) = 3 3
After exec $VAR1 = [['A','B','C'],['AABVczAD1AALvTlAAA','AABVczAD1AALvTlAAB','AABVczAD1AALvTlAAC']];
Returned data$VAR1 = [['AK','AABVczAD1AALvTlAAA'],['BL','AABVczAD1AALvTlAAB'],['CM','AABVczAD1AALvTlAAC']];