How to run Oracle PL/SQL in python - python

I'm using Jupyter notebook to run a PL/SQL script but I get an error. The code block in the notebook is as follows:
%%sql
DECLARE BEGIN
FOR record_item IN (
SELECT
*
FROM
duplicated_records
) LOOP
EXECUTE IMMEDIATE 'UPDATE table_name SET record_id ='|| record_item.original_record_id || ' WHERE record_id =' || record_item.duplicated_record_id;
EXECUTE IMMEDIATE 'DELETE FROM records WHERE id ='|| record_item.duplicated_record_id;
END LOOP;
END
The error is
(cx_Oracle.DatabaseError) ORA-06550: line 8, column 165:
PLS-00103: Encountered the symbol "end-of-file" when expecting one of the following:
Non-PL/SQL code, such as select, and update statements, seems to work.
It works perfectly fine with other SQL clients like SQL developer. I've tried adding/removing the; at the end but it still doesn't work.

I don't know Python so I can't assist about that, but - as far as Oracle is concerned - you don't need DECLARE (as you didn't declare anything), and you certainly don't need dynamic SQL (EXECUTE IMMEDIATE) as there's nothing dynamic there.
Rewritten:
BEGIN
FOR record_item IN (SELECT * FROM duplicated_records) LOOP
UPDATE table_name
SET record_id = record_item.original_record_id
WHERE record_id = record_item.duplicated_record_id;
DELETE FROM records
WHERE id = record_item.duplicated_record_id;
END LOOP;
END;
On the other hand, row-by-row processing is slow-by-slow. Consider using two separate statements: one which will update existing rows, and another which will delete rows (from a different table, apparently):
merge into table_name a
using duplicated_records b
on (a.record_id = b.duplicate_record_id)
when matched then update set
a.record_id = b.original_record_id;
delete from records a
where a.id in (select b.duplicated_record_id from duplicated_records b);
If tables are properly indexed (on ID columns), that should behave better (faster).

The direct implementation of your code in Python would be like:
import oracledb
import traceback
import os
import sys
#if sys.platform.startswith('darwin'):
# oracledb.init_oracle_client(lib_dir=os.environ.get('HOME')+'/Downloads/instantclient_19_8')
un = os.environ.get('PYTHON_USERNAME')
pw = os.environ.get('PYTHON_PASSWORD')
cs = os.environ.get('PYTHON_CONNECTSTRING')
try:
connection = oracledb.connect(user=un, password=pw, dsn=cs)
with connection.cursor() as cursor:
plsql = """BEGIN
FOR RECORD_ITEM IN (
SELECT
*
FROM
DUPLICATED_RECORDS
) LOOP
EXECUTE IMMEDIATE 'UPDATE table_name SET record_id ='
|| RECORD_ITEM.ORIGINAL_RECORD_ID
|| ' WHERE record_id ='
|| RECORD_ITEM.DUPLICATED_RECORD_ID;
EXECUTE IMMEDIATE 'DELETE FROM records WHERE id ='
|| RECORD_ITEM.DUPLICATED_RECORD_ID;
END LOOP;
END;"""
cursor.execute(plsql)
except oracledb.Error as e:
error, = e.args
traceback.print_tb(e.__traceback__)
print(error.message)
For this you need to install the oracledb module, which is the renamed, latest version of the cx_Oracle module. It will work with cx_Oracle by changing the import to import cx_Oracle as oracledb.
However, before blindly copying this, check #littlefoot's answer for more about the PL/SQL code.

Related

Is there a proper way to handle cursors returned from a postgresql function in psycopg?

I'm trying to make friends with postgresql (14.0 build 1914 64-bit on windows), psycopg2 (2.9.1 installed using pip) and python 3.8.10 on windows.
I have created a postgresql function in a database that returns a cursor, somthing like below
CREATE get_rows
...
RETURNS refcursor
...
DECLARE
res1 refcursor;
BEGIN
OPEN res1 FOR
SELECT some_field, and_another_field FROM some_table;
RETURN res1;
END
The function can be run from pgAdmin4 Quert tool
SELECT get_rows();
and will then return a cursor like "<unnamed portal 1>"
Still within query tool in pgAdmin4 I can issue:
BEGIN;
SELECT get_rows();
FETCH ALL IN "<unnamed portal 2>"; -- Adjust counter number
And this will get me the rows returned by the cursor.
Now I want to replicate this using psycopg instead of pgAdmin4
I have the below code
conn = psycopg2.connect("dbname='" + db_name + "' "\
"user='" + db_user + "' " +\
"host='" + db_host + "' "+\
"password='" + db_passwd + "'")
cursor = conn.cursor()
cursor.callproc('get_rows')
print("cursor.description: ", end = '')
print(cursor.description)
for record in cursor:
print("record: ", end = '')
print (record)
The above code only gives the cursor string name (as returned by the postgresql function 'get_rows') in the single record of the cursor created by psycopg.
How can I get a cursor-class object from psycopg that provides access the cursor returned by 'get_rows'?
https://www.psycopg.org/docs/cursor.html says cursor.name is read-only and I dont see an obvious way to connect the cursor from 'get_rows' with a psycopg cursor-instance
The cursor link you show refers to the Python DB API cursor not the Postgres one. There is an example of how to do what you want here Server side cursor in section:
Note It is also possible to use a named cursor to consume a cursor created in some other way than using the DECLARE executed by execute(). For example, you may have a PL/pgSQL function returning a cursor:
CREATE FUNCTION reffunc(refcursor) RETURNS refcursor AS $$
BEGIN
OPEN $1 FOR SELECT col FROM test;
RETURN $1;
END;
$$ LANGUAGE plpgsql;
You can read the cursor content by calling the function with a regular, non-named, Psycopg cursor:
cur1 = conn.cursor()
cur1.callproc('reffunc', ['curname'])
and then use a named cursor in the same transaction to “steal the cursor”:
cur2 = conn.cursor('curname')
for record in cur2: # or cur2.fetchone, fetchmany...
# do something with record
pass
UPDATE
Be sure and close the named cursor(cur2) to release the server side cursor. So:
cur2.close()

python sqlite - deleting selected records

I'm trying to use sqlite3 in python to delete a selection of rows from a table. My attempt fails, but I can't work out why.
The sql query works ok, but I can't implement it within the python code.
I have a set of records that are moved from current_table to archive_table after a period of time.
I'd like to clean up the current_table by removing those rows that are in the archive_table (matched on id).
Intended SQL query:
DELETE FROM current_table WHERE id IN ( SELECT id FROM archive_table);
Attempted python code:
import sqlite3
def clean_up(current_table, archive_table):
db = sqlite3.connect(sqlite_db)
cursor = db.cursor()
sql_query_delete = '''DELETE FROM %s WHERE id IN ( SELECT id FROM %s);''' % (current_table, archive_table)
try:
cursor.execute(sql_query_delete)
db.commit()
db.close()
except:
print("error deleting")
Now working. The database file was locked by another process. Removing the pointless try/except led me to the detailed error message.

Creating Columns with pyodbc - Not Working

I have a class which uses the pyodbc library successfully - it can perform a variety of reads from the database (so the connection and DSN are hunky dory).
What I've being trying to implement are functions to write and delete columns from tables in a sql database (the same one I'm able to read from).
I have tested the calls using isql commands and I can see the changes occur in my database. For example;
SQL> ALTER TABLE DunbarGen ADD testCol float(4)
SQLRowCount returns -1
Adds a new column to the table from the terminal (this works). I have a code which, I think, should replicate this command - which causes no errors in my class - and looks like this;
def createColumn(self, columnName, tableName, isFloat, isDateTime, isString):
if isFloat:
typeOf = 'float(4)'
elif isDateTime:
typeOf = 'datetime2'
elif isString:
typeOf = 'text'
else:
return False
self.cursor.execute("ALTER TABLE " + tableName + " ADD " + columnName + " " + typeOf)
print 'command has executed'
Do I need to do something else with the pyodbc class to finalize the command or something?
Thanks!
self.cursor.commit()
After the execute function has been called.

using python 2.7 to query sqlite3 database and getting "sqlite3 operational error no such table"

My simple test code is listed below. I created the table already and can query it using the SQLite Manager add-in on Firefox so I know the table and data exist. When I run the query in python (and using the python shell) I get the no such table error
def TroyTest(self, acctno):
conn = sqlite3.connect('TroyData.db')
curs = conn.cursor()
v1 = curs.execute('''
SELECT acctvalue
FROM balancedata
WHERE acctno = ? ''', acctno)
print v1
conn.close()
When you pass SQLite a non-existing path, it'll happily open a new database for you, instead of telling you that the file did not exist before. When you do that, it'll be empty and you'll instead get a "No such table" error.
You are using a relative path to the database, meaning it'll try to open the database in the current directory, and that is probably not where you think it is..
The remedy is to use an absolute path instead:
conn = sqlite3.connect('/full/path/to/TroyData.db')
You need to loop over the cursor to see results:
curs.execute('''
SELECT acctvalue
FROM balancedata
WHERE acctno = ? ''', acctno)
for row in curs:
print row[0]
or call fetchone():
print curs.fetchone() # prints whole row tuple
The problem is the SQL statment. you must specify the db name and after the table name...
'''SELECT * FROM db_name.table_name WHERE acctno = ? '''

How I can make same results on cx_Oracle and Sql * Plus?

It's - for sqlplus - commands:
SQL> set serveroutput on
SQL> exec where.my_package.ger_result('something');
something=1823655138
And it's - for cx_Oracle:
>>> c.callproc('where.my_package.ger_result', ('something',))
['something']
As You can see - the results are different.
I have no idea, how to fix it. :[
import cx_Oracle
dsn_tns = cx_Oracle.makedsn('my_ip_address_server_next_port', 0000, 'sid')
db = cx_Oracle.connect('user', 'password', dsn_tns)
curs = db.cursor()
curs.callproc("dbms_output.enable")
curs.callproc('where.my_package.ger_result', ['something',])
statusVar = curs.var(cx_Oracle.NUMBER)
lineVar = curs.var(cx_Oracle.STRING)
while True:
curs.callproc("dbms_output.get_line", (lineVar, statusVar))
if statusVar.getvalue() != 0:
break
print lineVar.getvalue()
Sorry, I can't reproduce this one.
I don't have your PL/SQL package, so I used the following stored procedure instead:
CREATE OR REPLACE PROCEDURE p_do_somet (
p_param IN VARCHAR2
) AS
BEGIN
dbms_output.put_line(p_param || '=1823655138');
END;
/
I got the same output, something=1823655138, from SQL*Plus and from using the Python script in your answer.
If you're getting different results using SQL*Plus and cx_Oracle, then either your stored procedure is doing something very funny (I don't know what could cause it to do this), or your SQL*Plus session and Python script are not connecting to the same database and/or schema.

Categories