How to use in statement with nested table - sql

Hey there I have a function, and part of the function is to make sure that the selected value is within the passed in table of varchar2s. To start I declare a varchar2 table type like so.
create or replace type Varchar2Table is table of varchar2(200)
Then I have the function which accepts the nested table parameter and has a select statement on them.
function SelectPeople(inputNames Varchar2Table) return People
begin
--stuff
select * from person_table where name in inputNames; --line of interest
--more stuff
end;
This doesn't seem to work though, I get the following error:
ORA-00932: inconsistent datatypes: expected NUMBER got
ENGSPL5.VARCHAR2TABLE
Any suggestions?

The TABLE operator allows nested tables to be used in SQL statements. The function was also missing an IS and an INTO.
create or replace type Varchar2Table is table of varchar2(200);
create table person_table(id number, name varchar2(100));
create or replace function SelectPeople(inputNames Varchar2Table) return number
is --Missing "IS".
type numberTable is table of number; --Need a collection to store results.
numbers numberTable;
begin
select id
bulk collect into numbers --Missing "INTO".
from person_table
where name in (select column_value from table(inputNames)); --Missing "TABLE".
--Alternatively a multiset condition can be used.
--where name member of inputNames;
--Dummy return value to make the function compile.
return 1;
end;
/

Related

Dynamic query that uses CTE gets "syntax error at end of input"

I have a table that looks like this:
CREATE TABLE label (
hid UUID PRIMARY KEY DEFAULT UUID_GENERATE_V4(),
name TEXT NOT NULL UNIQUE
);
I want to create a function that takes a list of names and inserts multiple rows into the table, ignoring duplicate names, and returns an array of the IDs generated for the rows it inserted.
This works:
CREATE OR REPLACE FUNCTION insert_label(nms TEXT[])
RETURNS UUID[]
AS $$
DECLARE
ids UUID[];
BEGIN
CREATE TEMP TABLE tmp_names(name TEXT);
INSERT INTO tmp_names SELECT UNNEST(nms);
WITH new_names AS (
INSERT INTO label(name)
SELECT tn.name
FROM tmp_names tn
WHERE NOT EXISTS(SELECT 1 FROM label h WHERE h.name = tn.name)
RETURNING hid
)
SELECT ARRAY_AGG(hid) INTO ids
FROM new_names;
DROP TABLE tmp_names;
RETURN ids;
END;
$$ LANGUAGE PLPGSQL;
I have many tables with the exact same columns as the label table, so I would like to have a function that can insert into any of them. I'd like to create a dynamic query to do that. I tried that, but this does not work:
CREATE OR REPLACE FUNCTION insert_label(h_tbl REGCLASS, nms TEXT[])
RETURNS UUID[]
AS $$
DECLARE
ids UUID[];
query_str TEXT;
BEGIN
CREATE TEMP TABLE tmp_names(name TEXT);
INSERT INTO tmp_names SELECT UNNEST(nms);
query_str := FORMAT('WITH new_names AS ( INSERT INTO %1$I(name) SELECT tn.name FROM tmp_names tn WHERE NOT EXISTS(SELECT 1 FROM %1$I h WHERE h.name = tn.name) RETURNING hid)', h_tbl);
EXECUTE query_str;
SELECT ARRAY_AGG(hid) INTO ids FROM new_names;
DROP TABLE tmp_names;
RETURN ids;
END;
$$ LANGUAGE PLPGSQL;
This is the output I get when I run that function:
psql=# select insert_label('label', array['how', 'now', 'brown', 'cow']);
ERROR: syntax error at end of input
LINE 1: ...SELECT 1 FROM label h WHERE h.name = tn.name) RETURNING hid)
^
QUERY: WITH new_names AS ( INSERT INTO label(name) SELECT tn.name FROM tmp_names tn WHERE NOT EXISTS(SELECT 1 FROM label h WHERE h.name = tn.name) RETURNING hid)
CONTEXT: PL/pgSQL function insert_label(regclass,text[]) line 19 at EXECUTE
The query generated by the dynamic SQL looks like it should be exactly the same as the query from static SQL.
I got the function to work by changing the return value from an array of UUIDs to a table of UUIDs and not using CTE:
CREATE OR REPLACE FUNCTION insert_label(h_tbl REGCLASS, nms TEXT[])
RETURNS TABLE (hid UUID)
AS $$
DECLARE
query_str TEXT;
BEGIN
CREATE TEMP TABLE tmp_names(name TEXT);
INSERT INTO tmp_names SELECT UNNEST(nms);
query_str := FORMAT('INSERT INTO %1$I(name) SELECT tn.name FROM tmp_names tn WHERE NOT EXISTS(SELECT 1 FROM %1$I h WHERE h.name = tn.name) RETURNING hid', h_tbl);
RETURN QUERY EXECUTE query_str;
DROP TABLE tmp_names;
RETURN;
END;
$$ LANGUAGE PLPGSQL;
I don't know if one way is better than the other, returning an array of UUIDs or a table of UUIDs, but at least I got it to work one of those ways. Plus, possibly not using a CTE is more efficient, so it may be better to stick with the version that returns a table of UUIDs.
What I would like to know is why the dynamic query did not work when using a CTE. The query it produced looked like it should have worked.
If anyone can let me know what I did wrong, I would appreciate it.
... why the dynamic query did not work when using a CTE. The query it produced looked like it should have worked.
No, it was only the CTE without (required) outer query. (You had SELECT ARRAY_AGG(hid) INTO ids FROM new_names in the static version.)
There are more problems, but just use this query instead:
INSERT INTO label(name)
SELECT unnest(nms)
ON CONFLICT DO NOTHING
RETURNING hid;
label.name is defined UNIQUE NOT NULL, so this simple UPSERT can replace your function insert_label() completely.
It's much simpler and faster. It also defends against possible duplicates from within your input array that you didn't cover, yet. And it's safe under concurrent write load - as opposed to your original, which might run into race conditions. Related:
How to use RETURNING with ON CONFLICT in PostgreSQL?
I would just use the simple query and replace the table name.
But if you still want a dynamic function:
CREATE OR REPLACE FUNCTION insert_label(_tbl regclass, _nms text[])
RETURNS TABLE (hid uuid)
LANGUAGE plpgsql AS
$func$
BEGIN
RETURN QUERY EXECUTE format(
$$
INSERT INTO %s(name)
SELECT unnest($1)
ON CONFLICT DO NOTHING
RETURNING hid
$$, _tbl)
USING _nms;
END
$func$;
If you don't need an array as result, stick with the set (RETURNS TABLE ...). Simpler.
Pass values (_nms) to EXECUTE in a USING clause.
The tablename (_tbl) is type regclass, so the format specifier %I for format() would be wrong. Use %s instead. See:
Table name as a PostgreSQL function parameter

ORA-01460 unimplemented or unreasonable conversion requested

I am passing ID's in oracle proc. ID's can be in 1000's. currently its able to process around 600 ID's, if I pass more than 600 ID's - I am getting ORA-01460 unimplemented or unreasonable conversion requested. ID is varchar2 datatype, how can I process 1000's of Id's in varchar2 or what will be the best strategy to handle this kind of issue. Any guidance/suggestion will be highly appreciated. Can this be solved using CLOB datatype?
//this is how I am processing Id's
create or replace procedure Emp(
emp_id in varchar2
)
//passing those id's in CTE before passing to subquery
WITH
EMP_LIST AS(
select regexp_substr(emp_id,'[^,]+', 1, level) from dual
connect by level <= LENGTH(regexp_substr(emp_id, '[^,]+'))+1
)
Pass a collection or VARRAY rather than passing a comma-delimited string:
CREATE TYPE number_list IS TABLE OF NUMBER(10,0);
Then you can use it something like:
CREATE PROCEDURE emp(
emp_ids IN number_list
)
IS
BEGIN
-- Do something with the ids like inserting them into a table
INSERT INTO employees ( id )
SELECT COLUMN_VALUE
FROM TABLE( emp_ids );
-- Or something like this:
SELECT something
INTO some_variable -- you need to define this variable first
FROM some_table
WHERE emp_id MEMBER OF emp_ids;
END;
/
Update
If you can't create anything then you can use a built-in collection like SYS.ODCINUMBERLIST:
CREATE PROCEDURE emp(
emp_ids IN SYS.ODCINUMBERLIST
)
IS
BEGIN
-- Do something with the ids like inserting them into a table
INSERT INTO employees ( id )
SELECT COLUMN_VALUE
FROM TABLE( emp_ids );
-- Or something like this:
SELECT something
INTO some_variable -- you need to define this variable first
FROM some_table
WHERE emp_id IN ( SELECT COLUMN_VALUE FROM TABLE( emp_ids ) );
END;
/
(Note: SYS.ODCI*LIST types are VARRAY data types and do not support the MEMBER OF operator like collections do; instead you can get the values from the VARRAY using a nested SELECT statement with a TABLE() collection expression.)
However, if you really can't CREATE anything then you won't be able to CREATE PROCEDURE .... not sure there is any solution to that apart from talking to your DBA.

Oracle procedure, placement of multiple records in variables

I'm trying to create my first oracle procedure. The select will return multiple records; I need to be able to place each record in the variables and use the record in later actions in the procedure. Any help please?
key number;
keyCount number;
rub varchar2(50);
srub varchar2(100);
type varchar2(200);
date varchar2(14);
note varchar2(500);
BEGIN
SELECT KEY,COUNT(KEY),RUB,
SRUB,TYPE ,DATE,NOTE FROM Student
WHERE S_KEY = {key};
END;
In PL/SQL we need to select results into matching variables. One way is separate variables for each column (as shown). The alternative is to use a row variable which matches the project of the query; find out more.
You've got an aggregating function - COUNT() so you need a GROUP BY clause which defines the non-aggregating columns. You say you have more than one record so you need to populate a collection not scalar variables. Find out more.
Your procedure should look something like this
create or replace procedure my_first_proc
( p_key in student.s_key%type )
as
type my_rec is record (
key number ,
keyCount number ,
rub varchar2(50); ,
srub varchar2(100) ,
type varchar2(200) ,
date varchar2(14),
note varchar2(500)
);
type my_rec_coll is table of my_rec;
l_student_recs my_rec_coll;
BEGIN
SELECT KEY,COUNT(KEY),RUB,SRUB,TYPE ,DATE,NOTE
bulk collect into l_student_recs
FROM Student
WHERE S_KEY = p_key
group by KEY,RUB,SRUB,TYPE ,DATE,NOTE
;
for idx in l_student_recs.first() .. l_student_recs.last()
loop
-- do some processing here
dbms_output.put_line('RUB = '||l_student_recs(idx).rub);
end loop;
EXCEPTION
when no_data_found then
raise_application_error(-01403, 'no student records for key='||p_key);
END;
Get into good habits:
use sensible variable names
distinguish parameter names from local variables
handle predictable exceptions

Where column in <function returns a collection > in oracle

I want to achieve something like this:
Example:
Table Customers has columns customer_no, name , age.
some_package package has the following types defined in its spec:
type cust_type is record (custs Customers.customer_no);
type rec_type is table of cust_type index by binary_integer;
function some_function return rec_type;
I am trying to create a view the goes like this:
select ....
from customers c, tablex, tabley
where c.customer_no in some_function() and
... <<other clauses>>
I cannot avoid using some_function() as the logic uses dynamic SQL statements.
I get invalid data type error when i try to compile the view
Is it possible to achieve this in Oracle sql? I don't want to use another function and loops to do this.
Thanks.
No, type rec_type is declared in a package and can be used in PL/SQL blocks but cannot be used in SQL statements.
If you want to use a type in SQL statements then you will need to declare it using a CREATE TYPE statement like this:
CREATE TYPE customers_tab IS TABLE OF NUMBER;
or you can use an existing type like SYS.ODCINUMBERLIST.
Then change the package to:
CREATE OR REPLACE PACKAGE some_package
AS
FUNCTION some_function RETURN SYS.ODCINUMBERLIST;
END;
/
CREATE OR REPLACE PACKAGE BODY some_package
AS
FUNCTION some_function RETURN SYS.ODCINUMBERLIST
AS
t_customers SYS.ODCINUMBERLIST;
BEGIN
SELECT customer_no
BULK COLLECT INTO t_customers
FROM customers
WHERE MOD( customer_no, 3 ) = 0; -- or whatever your query is.
RETURN t_customers;
END;
END;
/
Then you can do:
SELECT *
FROM Customers c
INNER JOIN
TABLE( some_package.some_function() ) t
ON ( c.customer_no = t.COLUMN_VALUE );

oracle inline function called multiple times

When calling a function via an inline select statement, when the function is returning a custom type, Oracle seems to execute the function equal to the number of arguments +1. This seems to happen when the select is included as a CTAS or an insert/select.
Has anyone seen this before? Is this an Oracle bug? I would expect the function to be called once per row in the table.
--Inline function gets called for the number of arguments +1
--drop table t
create table t(
id number,
l_blob blob
);
insert into t values(1, utl_raw.cast_to_raw('SampleString'));
COMMIT;
create table tmp_ts (c1 timestamp);
create or replace type test_type as object(
c1 varchar2(32)
,c2 varchar2(32)
);
/
create or replace FUNCTION test_function (p_blob blob, p_date date)
RETURN test_type
IS
BEGIN
--This could also be a DBMS_OUTPUT.PUT_LINE statement
insert into tmp_ts VALUES (systimestamp);
return test_type(null,null);
END test_function;
/
--0
select count(*) from tmp_ts;
--Call function on 1 row table - function should just insert 1 row into tmp_ts
create table tst_table as
select test_function(l_blob, '25-JAN-09') as c1
from t;
--it actually inserts 3
select count(*) from tmp_ts;
Example where increasing the argument call for the type increases the number of time the function is executed
--Same example with more arguements - 6 arguements here
--Inline function gets called for the number of arguments +1
--drop table t
create table t2(
id number,
l_blob blob
);
insert into t2 values(1, utl_raw.cast_to_raw('SampleString'));
COMMIT;
create table tmp_ts2 (c1 timestamp);
create or replace type test_type2 as object(
c1 varchar2(32)
,c2 varchar2(32)
,c3 varchar2(32)
,c4 varchar2(32)
,c5 varchar2(32)
,c6 varchar2(32)
);
/
create or replace FUNCTION test_function2 (p_blob blob, p_date date)
RETURN test_type2
IS
BEGIN
insert into tmp_ts2 VALUES (systimestamp);
return test_type2(null,null,null,null,null,null);
END test_function2;
/
--0
select count(*) from tmp_ts2;
--Call function on 1 row table - function should just insert 1 row into tmp_ts
create table tst_table2 as
select test_function2(l_blob, '25-JAN-09') as c1
from t;
--it actually inserts 7
select count(*) from tmp_ts2;
Any help/feedback is greatly appreciated.
First: It is a bug that you can even perform a DML inside a function which is called in a SELECT Statement. This should raise an exception.
Otherwise Oracle makes absolutely no guarantee how often Functions in a SQL-Select are executed, it could be once per row, ten times per row or just once for the whole query (with caching) - so however often it is called, this conforms to the specifications.
In this special case it will call the function for each attribute of the returning type, since oracle will not insert the object type as one memory-structure, but use the function like a table with multiple columns and read each column individually like this:
INSERT VALUES ( myFunc(x).attribute1, myFunc(x).attribute2 );
The important part: Never make any assumptions about how often a FUNCTION is called when you use it in an SQL Statement!!! At any time the function could be called again by the optimizer, maybe for sampling or caching...
Preferred solution: Pipelined Functions - a pipelined Function can be called like a Table and will only be called once. You can pass in a cursor which the function uses for input processing and do the whole data-transformation and logging and everything in the function.