I am writing a user defined function in pl/sql for an Oracle database. The function purpose is converting an xml field to a clob. This is the code:
CREATE OR REPLACE FUNCTION IDS_ORA.castField_xml_to_clob (xmlField IN XMLTYPE)
RETURN CLOB
AS
BEGIN
RETURN XMLSERIALIZE(CONTENT xmlField AS CLOB NO INDENT)
END;
/
Compiling fails with this error:
PLS-00103: Encountered the symbol "XMLFIELD" when expecting one of the
following: . ( ) , * # % & = - + < / > at in is mod remainder not
rem => <> o != o ~= >= <= <> and or like like2
like4 likec as between from using || multiset member submultiset
Function:IDS_ORA.CASTFIELD_XML_TO_NVARCHAR(IDS_ORA#172.25.1.134:1522:sviluppo)
5 31
It seems a generic error to me, so I tried using XMLSERIALIZE directly inside a query and it works.
SELECT XMLSERIALIZE(CONTENT "AttributesValue" AS CLOB no INDENT)
FROM IDS_ORA."Job";
This means the syntax is correct, what is it complaining about?
XMLSerialize isn't a native PL/SQL function. Many SQL functions can be called directly in PL/SQL, such as to_date(); unfortunately this isn't one of them. (I don't think any of the XML-related functions can be, in fact).
In PL/SQL expressions, you can use all SQL functions except:
...
XML functions (such as APPENDCHILDXML and EXISTSNODE)
You can't use direct assignment, so you need to use a query:
CREATE OR REPLACE FUNCTION castField_xml_to_clob (xmlField IN XMLTYPE)
RETURN CLOB
AS
result CLOB;
BEGIN
SELECT XMLSERIALIZE(CONTENT xmlField AS CLOB NO INDENT)
INTO result
FROM dual;
RETURN result;
END;
/
Function CASTFIELD_XML_TO_CLOB compiled
Which adds another context switch, so unless you're trying to have a common place to control indent options for all formatted XML so you can change them all in one place, I'm not sure this gains you anything over just calling XMLSerialize directly wherever you were going to call this UDF.
Related
I was trying to create a Snowflake SQL UDF
Where it computes the Values of the all values and will return the result to the user.
So firstly, i have tried the following approach
# The UDF that Returns the Result.
CREATE OR REPLACE FUNCTION PRODUCT_OF_COL_VAL()
RETURNS FLOAT
LANGUAGE SQL
AS
$$
SELECT EXP(SUM(LN(COL))) AS RESULT FROM SCHEMA.SAMPLE_TABLE
$$
The above code executes perfectly fine....
if you could see above (i have hardcoded the TABLE_NAME and COLUMN_VALUE) which is not i acutally want..
So, i have tried the following approach, by passing the column name dynamically..
create or replace function (COL VARCHAR)
RETURNS FLOAT
LANGUAGE SQL
AS
$$
SELECT EXP(SUM(LN(COL))) AS RESULT from SCHEMA.SAMPLE_TABLE
$$
But it throws the following issue...
Numeric Value 'Col' is not recognized
To elaborate more the Data type of the Column that i am passing is NUMBER(38,6)
and in the background its doing the following work..
EXP(SUM(LN(TO_DOUBLE(COL))))
Does anyone have any idea why this is running fine in Scenario 1 and not in Scenario 2 ?
Hopefully we will be able to have this kind of UDFs one day, in the meantime consider this answer using ARRAY_AGG() and a Python UDF:
Sample usage:
select count(*) how_many, multimy(array_agg(score)) multiplied, tags[0] tag
from stack_questions
where score > 0
group by tag
limit 100
The UDF in Python - which also protects against numbers beyond float's limits:
create or replace function multimy (x array)
returns float
language python
handler = 'x'
runtime_version = '3.8'
as
$$
import math
def x(x):
res = math.prod(x)
return res if math.log10(res)<308 else 'NaN'
$$
;
The parameter you defined in SQL UDF will be evaluated as a literal:
When you call the function like PRODUCT_OF_COL_VAL('Col'), the SQL statement you execute becomes:
SELECT EXP(SUM(LN('Col'))) AS RESULT from SCHEMA.SAMPLE_TABLE
What you want to do is to generate a new SQL based on parameters, and it's only possible using "stored procedures". Check this one:
Dynamic SQL in a Snowflake SQL Stored Procedure
On an Oracle DB I have a table with SDO_GEOMETRY objects. I would like to query the database for those polygons with less than x edges. In theory this would be easy with a query like
SELECT * FROM myTable t WHERE LENGTH(t.geometry.sdo_ordinates) < x
Obviously the LENGTH funtion is defined for char and the type of
t.geometry.sdo_ordinates is oracle.sql.ARRAY so that doesn't work. Shouldn't there be a trivial way to SELECT the length or an array in Oracle? Somehow I'm unable to get the syntax right.
PS: I kind of solved my search with the following query, still the original questerion remains, isn't there an array size/length function?
SELECT * FROM myTable t WHERE LENGTH(t.geomety.Get_WKT()) < (x * c)
No, there is no simple sql function that counts the elements of an array.
However as mentioned here, another idea is a PL/SQL script.
create or replace function get_count(ar in SDO_ORDINATE_ARRAY) return number is
begin
return ar.count;
end get_count;
t.geometry.sdo_ordinates.COUNT is a PL/SQL attribute that can be used within functions/procedures. Thus that is not a function useable in plain SQL.
Attribute:
value.someAttribute
Function:
doSomething(value)
Clarification: Functions have return values, procedures don't. Source
I'm trying to create a package that works out the number of days between two dates, I'm aware I have probably got this miles wrong, I am really struggling to troubleshoot this. My knownledge is low on oracle, I'm still quite new to this. The package I've written is below, but I am getting the error shown at the bottom.
How do I resolve this?
CREATE OR REPLACE PACKAGE PACKNAME
AS
FUNCTION TIME_SCALE RETURN NUMBER;
END;
/
CREATE OR REPLACE PACKAGE BODY PACKNAME
AS closed_date := '28-APR-14'
FUNCTION TIME_SCALE RETURN NUMBER;
IS BEGIN
TRUNC(mrc.closed_date - mrc.open_date) AS days_difference FROM TASKS mrc;
END;
Error at line 2: PLS-00103: Encountered the symbol "=" when expecting one of the following: constant exception <an identifier> <a double-quoted delimited-identifier> table long double ref char time timestamp interval date binary national character nchar
I have substituted some of the names to make them clearer for you to read.
The function is to output the number of days it has taken for a task to be completed. The columns basically include the date_opened and date_closed in simple terms as well as a few others and a unique ID which I believe is a sequence.
Try this:
CREATE OR REPLACE PACKAGE PACKNAME
AS
FUNCTION TIME_SCALE
RETURN NUMBER;
END;
/
CREATE OR REPLACE PACKAGE BODY PACKNAME
AS
closed_date VARCHAR2(50):= '28-APR-14';
days_difference NUMBER;
FUNCTION TIME_SCALE
RETURN NUMBER
IS
BEGIN
SELECT TRUNC(mrc.closed_date - mrc.open_date) INTO days_difference
FROM TASKS mrc;
RETURN days_difference;
END;
END;
What was wrong:
1) You have missed the type for closed_date
2) You had an ';' after RETURN NUMBER in function declaration
3) You have missed SELECT clause inside function
4) You have missed END for the package
I am trying to call a function within a function using sql on postgres 9.3.
This question is related to another post by me.
I have written the below function. So far I have failed to incorporate any kind of save-output (COPY) statement, so I am trying to work around this by creating a nested function print-out function.
CREATE FUNCTION retrieve_info(TEXT, TEXT) RETURNS SETOF
retrieve_info_tbl AS $$
SELECT tblA.id, tblA.method, tblA.species, tblA.location
FROM tblA
WHERE method=$1 AND species=$2
GROUP BY id, method, species
ORDER BY location
$$ LANGUAGE 'sql';
The above function works.
An attempt to create a nested function.
CREATE FUNCTION print_out(TEXT, TEXT) RETURNS void AS $$
COPY (SELECT * FROM retrieve_info($1, $2)) TO 'myfilepath/test.csv'
WITH CSV HEADER;
$$ LANGUAGE 'sql';
Calling nested function.
SELECT * FROM print_out('mtd1','sp1');
OUTPUT
The above gives this ERROR: column "$1" does not exist SQL state: 42P02 Context: SQL function "print_out" statement 1. However, when substituting the arg1, arg2 in print_out() with 'mtd1','sp1' the correct output is printed to test.csv (as seen below)
id | method | ind | location
----------------------------
1a | mtd1 | sp3 | locA
1d | mtd1 | sp3 | locB
How would I get the arg1, arg2 of retrieve_info() to call arg1, arg2 properly within print_out()?
I am completely stuck. Would appreciate any pointers, thanks
COPY is a bit odd as it sort of treats its query argument as a string even though it isn't written as a string. The result is that the query:
SELECT * FROM retrieve_info($1, $2)
isn't executed in the context of the function, it is executed in the context of COPY itself. Even though you say:
copy (select * from t) ...
it is treated more as though you wrote:
copy 'select * from t' ...
so by the time the query is executed, the function parameters no longer have any meaning, the query argument to COPY may look like it would behave like a closure in other languages but it doesn't, it acts more like a string that gets passed to eval.
You can get around this strangeness by using the usual Kludge of Last Resort: dynamic SQL. You should get better results if you write your function to use string wrangling and EXECUTE:
create or replace function print_out(text, text) returns void as $$
begin
execute 'copy ('
|| 'select * from retrieve_info'
|| '(' || quote_literal($1) || ',' || quote_literal($2) || ')'
|| ') to ''myfilepath/test.csv'' with csv header;';
end;
$$ language plpgsql;
Are x and y quoted intentionally?
COPY (SELECT * FROM retrieve_info('x','y')) TO 'myfilepath/test.csv'
You are not sending the x and y arguments of print_out to retrieve_info - rather, you are sending the strings 'x' and 'y'. Assuming you don't have records with method='x' AND species='y', it's of little wonder you get no results.
Try this instead:
COPY (SELECT * FROM retrieve_info(x,y)) TO 'myfilepath/test.csv'
I want to call a function by passing multiple values on single parameter, like this:
SELECT * FROM jobTitle('270,378');
Here is my function.
CREATE OR REPLACE FUNCTION test(int)
RETURNS TABLE (job_id int, job_reference int, job_job_title text
, job_status text) AS
$$
BEGIN
RETURN QUERY
select jobs.id,jobs.reference, jobs.job_title,
ltrim(substring(jobs.status,3,char_length(jobs.status))) as status
FROM jobs ,company c
WHERE jobs."DeleteFlag" = '0'
and c.id= jobs.id and c.DeleteFlag = '0' and c.active = '1'
and (jobs.id = $1 or -1 = $1)
order by jobs.job_title;
END;
$$ LANGUAGE plpgsql;
Can someone help with the syntax? Or even provide sample code?
VARIADIC
Like #mu provided, VARIADIC is your friend. One more important detail:
You can also call a function using a VARIADIC parameter with an array type directly. Add the key word VARIADIC in the function call:
SELECT * FROM f_test(VARIADIC '{1, 2, 3}'::int[]);
is equivalent to:
SELECT * FROM f_test(1, 2, 3);
Other advice
In Postgres 9.1 or later right() with a negative length is faster and simpler to trim leading characters from a string:
right(j.status, -2)
is equivalent to:
substring(j.status, 3, char_length(jobs.status))
You have j."DeleteFlag" as well as j.DeleteFlag (without double quotes) in your query. This is probably incorrect. See:
PostgreSQL Error: Relation already exists
"DeleteFlag" = '0' indicates another problem. Unlike other RDBMS, Postgres properly supports the boolean data type. If the flag holds boolean data (true / false / NULL) use the boolean type. A character type like text would be inappropriate / inefficient.
Proper function
You don't need PL/pgSQL here. You can use a simpler SQL function:
CREATE OR REPLACE FUNCTION f_test(VARIADIC int[])
RETURNS TABLE (id int, reference int, job_title text, status text)
LANGUAGE sql AS
$func$
SELECT j.id, j.reference, j.job_title
, ltrim(right(j.status, -2)) AS status
FROM company c
JOIN job j USING (id)
WHERE c.active
AND NOT c.delete_flag
AND NOT j.delete_flag
AND (j.id = ANY($1) OR '{-1}'::int[] = $1)
ORDER BY j.job_title
$func$;
db<>fiddle here
Old sqlfiddle
Don't do strange and horrible things like converting a list of integers to a CSV string, this:
jobTitle('270,378')
is not what you want. You want to say things like this:
jobTitle(270, 378)
jobTitle(array[270, 378])
If you're going to be calling jobTitle by hand then a variadic function would probably be easiest to work with:
create or replace function jobTitle(variadic int[])
returns table (...) as $$
-- $1 will be an array if integers in here so UNNEST, IN, ANY, ... as needed
Then you can jobTitle(6), jobTitle(6, 11), jobTitle(6, 11, 23, 42), ... as needed.
If you're going to be building the jobTitle arguments in SQL then the explicit-array version would probably be easier to work with:
create or replace function jobTitle(int[])
returns table (...) as $$
-- $1 will be an array if integers in here so UNNEST, IN, ANY, ... as needed
Then you could jobTitle(array[6]), jobTitle(array[6, 11]), ... as needed and you could use all the usual array operators and functions to build argument lists for jobTitle.
I'll leave the function's internals as an exercise for the reader.