PostgreSQL: Function with multiple date parameter - sql

I'm trying to create a function with multiple parameter as below:
CREATE OR REPLACE FUNCTION select_name_and_date (
IN f_name character,
IN m_name character,
IN l_name character,
IN start_date date,
IN end_date date )
RETURNS TABLE (
start_date date ,first_name character, middle_name character,last_name character ) AS $BODY$
BEGIN RETURN QUERY
select a.start_date, a.first_name, a.middle_name, a.last_name
FROM table1 a
where code in ('NEW', 'OLD')
and ( (a.first_name like '%' || f_name || '%' and a.middle_name like '%' || m_name || '%' and a.last_name like '%' || l_name || '%'))
or ((a.date_applied) between start_date and end_date );
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
When I tried to execute with date, it shows correct result.
select * from select_name_and_date ('Firstname','','','2016-06-27','2016-06-28');
When i tried to remove the value of date, it shows:
ERROR: invalid input syntax for type date: ""
select * from select_name_and_date ('Firstname','','','','');
When I tried to replace with NULL value of the date, it shows: 0 rows retrieved. (when it should have)
select * from select_name_and_date ('Firstname','','',NULL,NULL);
I want to have parameter that not depending on each parameter.

The between operator does not handle nulls. If you want to allow them, you'll to treat them explicitly. E.g., you could rewrite the part of the condition that applies to a.date_applied as follows:
((a.date_applied BETWEEN start_date AND end_date) OR
(start_date IS NULL AND a.date_applied < end_date) OR
(end_date IS NULL AND a.date_applied >= end_date) OR
(start_date IS NULL AND end_date IS NULL))

Related

Looping through list comma separated parameter and fetch results based on the every value before comma

The input parms are 2 ids(from_id and to_id) that are comma separated. Each id must be evaluated as the report is run . From_id='202031,202032,202035,202041,...'
To_id ='202111,202112,202135,202141,...'
Should filter the records for each from _id and while printing to log file the corresponding to_id should be printed. Always be 202031 to 202111, 202032 to 202112, 202035 to 202135, 20241 to 202141 . Below is the expected result.
Num_ber Description person_id person_name Rolled_To_term From_id to_id term_priority_to_term max_amt_to_term
90010001 fund1 1 abc N 202031 202111 3 23
90010001 fund1 1 abc N 202032 202112 6 110
50010001 fund2 2 xyz N 202035 202135 2 45
50010001 fund2 3 efg N 202035 202135 5 50
Below procedure updates the to_id ,but the from_id remains 202031 and gets looped again . The cursor does not filter for the other frm_id's.
PROCEDURE report_v1 (
p_frm_id_list VARCHAR2,
p_to_id_list VARCHAR2
) AS
exemp_log_file utl_file.file_type;
log_msg VARCHAR2(600);
frm_id_list VARCHAR2(50);
frm_id_list_temp VARCHAR2(50);
frm_id VARCHAR2(50);
to_id_list VARCHAR2(50);
to_id_list _temp VARCHAR2(50);
to_id VARCHAR2(50);
CURSOR get_expt_info_c IS
SELECT
code AS num_ber,
desc AS description,
a.term_expiration AS exp_term,
(select p_id from tab3 c where a.id=c.id) as person_id,
(select p_name from tab3 c where a.id=c.id) as person_name,
a.term_code AS from_id,
nvl(
(
SELECT DISTINCT
'Y'
FROM
tab2 b
WHERE
b.term_code = to_term
AND b.id = a.id
), 'N'
) rolled_to_term,
a.priority AS term_priority_to_term,
a.amount AS max_amt_to_term
FROM
tab left
JOIN tab2 a ON code = a.code
AND term_code = a.term_code
WHERE
instr(
frm_id, a.term_code
) > 0;
BEGIN
exemp_log_file := utl_file.fopen(dir,'report.csv','w');
log_msg := 'Report header';
utl_file.put_line(exemp_log_file,log_msg,autoflush => true );
frm_id_list := replace(p_frm_id_list ,' ','');
to_id_list := replace( p_ to_id_list, ' ','' );
WHILE frm_id_list IS NOT NULL LOOP
IF instr( frm_id_list,',') > 0 THEN
frm_id := substr(frm_id_list,1,instr(frm_id_list,',') - 1 );
frm_id_list_temp := substr(frm_id_list,instr(frm_id_list,',' ) + 1);
ELSE
frm_id := frm_id_list;
frm_id_list_temp := NULL;
END IF;
frm_id_list := frm_id_list_temp;
WHILE to_id_list IS NOT NULL LOOP
IF instr(to_id_list,',' ) > 0 THEN
to_id := substr(to_id_list,1,instr(to_id_list,',' ) - 1 );
to_id_list_temp := substr(to_id_list,instr(to_id_list,',' ) + 1 );
ELSE
to_id := to_id_list;
to_id_list_temp := NULL;
END IF;
to_id_list := to_id_list_temp;
FOR exemp IN get_expt_info_c LOOP
utl_file.put_line(
exemp_log_file,
exemp.num_ber
|| ';'
|| exemp.description
|| ';'
|| exemp.exp_term
|| ';'
|| exemp.person_id
|| ';'
|| exemp.person_name
|| ';'
|| exemp.from_id
|| ';'
|| exemp.rolled_to_term
|| ';'
|| exemp.priority
|| ';'
|| exemp.amt
|| ';'
|| to_id,
autoflush => true
);
END LOOP;
END LOOP;
END LOOP;
EXCEPTION
WHEN no_data_found THEN
NULL;
WHEN OTHERS THEN
EXIT;
utl_file.fclose(exemp_log_file);
END report_v1;
TABLE DEFINITION &Sample DATA
CREATE TABLE "TAB"
(CODE" VARCHAR2(8 CHAR) NOT NULL ENABLE,
"TERM_CODE" VARCHAR2(6 CHAR) NOT NULL ENABLE,
"DESC" VARCHAR2(30 CHAR) NOT NULL ENABLE,
"DETAIL_CODE" VARCHAR2(4 CHAR) NOT NULL ENABLE,
"ACTIVITY_DATE" DATE NOT NULL ENABLE
)
CODE TERM_CODE DESC DETAIL_CODE ACTIVITY_DATE
90010001 202031 fund1 aaa 09-Jun-2009
CREATE TABLE."TAB2"
("CODE" VARCHAR2(8 CHAR),
"ID" NUMBER(8,0),
"TERM_CODE" VARCHAR2(6 CHAR),
"ACTIVITY_DATE" DATE,
"TERM_EXPIRATION" VARCHAR2(6 CHAR),
"PRIORITY" NUMBER(2,0),
"AMOUNT" NUMBER(7,2)
)
CODE ID TERM_CODE ACTIVITY_DATE TERM_EXPIRATION PRIORITY AMOUNT
90010001 1 202031 09-Jun-2009 31-DEC-2030 3 23
CREATE TABLE "TAB3"
("P_ID" NUMBER(8,0) NOT NULL ENABLE,
"P_NAME" VARCHAR2(60 CHAR) NOT NULL ENABLE,
"ACTIVITY_DATE" DATE NOT NULL ENABLE
)
P_ID P_NAME ACTIVITY_DATE
1 abc 01-JAN-2007
Can anybody suggest a better way /alternative to my approach or correct my procedure.
Your description indicated you are splitting 2 CSV strings into their individual elements them combining the split elements into a set of 2 elements, one from each string. This can be accomplished in a single sql statement:
with parms( from_ids, to_ids) as
( select '202031,202032,202035,202041'
, '202111,202112,202135,202141'
from dual
)
select from_id, to_id
from ( select regexp_substr (from_ids, '[^,]+', 1, rownum) from_id
, regexp_substr (to_ids, '[^,]+', 1, rownum) to_id
, 1+length(from_ids)-length(replace(from_ids,',','')) cf
, 1+ length(to_ids)-length(replace(to_ids,',','')) ct
from parms
connect by level <= length (regexp_replace (from_ids, '[^,]+')) + 1
)
where cf=ct;
If the initial strings do not contain the same number of elements the query returns no rows.
I am not sure what to do with your procedure as what you have posted is invalid and did not produce any results other than compile errors. It does not reference the parameter "from_id_list" but does use the variants "frm_id_list" (defined) and "p_from_id_list" (not defined). As for the parameter "to_id_list" it attempts to use it an an LVALUE in assignment. Since it is defined as an IN parameter this is not allowed. Further the procedure does not refer to any local variables except "log_msg" and "exempt_log_file". Looks like you posted a prior version by mistake. You may want to edit your question and post the correct version.
I does appear however that the 2 WHILE loops a are attempting to do the split and combine process. The query above replaces both. But how does that fit into the cursor loop?
So, I have a project I built a while back that is pretty much identical to this one on github: https://github.com/DevNambi/sql-server-regex
You may want to download that, get it installed (C# deterministic regex functions wont break SQL Server), and then use Matches to get a result set of your matches and their values. With would allow something like this..
SELECT S.RelevantColumns, TRY_CONVERT(INT, Match) as ID
FROM SourceTable S
CROSS APPLY dbo.RegexMatches(S.IDlist, '(\d),') x
INNER JOIN dbo.SomeOtherTable OT
ON OT.ID = TRY_CONVERT(INT, x.Match)
I've done this a few times when a reporting layout was not conducive to iteration like your cursor above. Let me know if this helps. I'm unsure entirely what the purpose of the entire script is, but hopefully this provides some insight into how to achieve this?

To get column name in the result in postgresql

In the below query I am getting the values in a array format and I want to get column name too.
CREATE OR REPLACE FUNCTION load_page(IN _session integer)
RETURNS TABLE(col1 text, col2 text) AS
$BODY$
BEGIN
RETURN QUERY
select
(SELECT array_agg(sq.*)
FROM (SELECT user_id, user_name
FROM "user"
) sq
)::text,
(SELECT array_agg(sq.*)
FROM (SELECT client_id, client_name ,client_desc
FROM "clients"
) sq
)::text;
END;
$BODY$ LANGUAGE plpgsql STABLE;
The Result is :
"("{""(2,Test)"",""(5,Santhosh)"",""(3,Test1)""}","{""(1,Test1,Test1)"",""(2,test2,test2)"",""(3,test3,test3)""}")"
You could simply type them and concat into one column to use it as a row in outer select:
CREATE OR REPLACE FUNCTION load_page(IN _session integer)
RETURNS TABLE(col1 text, col2 text) AS
$BODY$
BEGIN
RETURN QUERY
select
(SELECT array_agg(sq.*)
FROM (SELECT concat('user_id: ',user_id,', user_name: ', user_name)
FROM "user"
) sq
)::text,
(SELECT array_agg(sq.*)
FROM (SELECT concat('client_id: ',client_id,', client_name: ',client_name,', client_desc: ',client_desc)
FROM "clients"
) sq
)::text;
END;
$BODY$ LANGUAGE plpgsql STABLE;
If you want to discard information about particular column if it's value is NULL you can append column names conditionally (with a CASE statement).
The below function does not produce the curly braces or the flood of double quotes, but it does include the column names and it is quite a bit more concise and readable and efficient than your initial version.
The _session parameter is never used, but I shall assume that this is all part of some larger query/function.
CREATE OR REPLACE FUNCTION load_page(IN _session integer, OUT col1 text, OUT col2 text)
RETURNS SETOF record AS $BODY$
BEGIN
SELECT string_agg('user_id: ' || user_id ||
', user_name: ' || user_name, ',')
INTO col1
FROM "user";
SELECT string_agg('client_id: ' || client_id ||
', client_name: ' || client_name ||
', client_desc: ' || client_desc, ',')
INTO col2
FROM "clients";
RETURN NEXT;
END;
$BODY$ LANGUAGE plpgsql STABLE;

How to address a computed column value in an SQLite query?

Having a following table:
CREATE TABLE `foo`(
`year` INT NOT NULL,
`month` INT NOT NULL,
`day` INT NOT NULL,
`hour` INT NOT NULL,
`minute` INT NOT NULL,
`value` INT NOT NULL,
PRIMARY KEY(`year`, `month`, `day`, `hour`, `minute`)
);
I want to write a query which will add 2 columns for every record: date for standard single-column data representation and weekday to indicate day-of-a-week number.
I have tried
SELECT
`year`, `month`, `day`, `hour`, `minute`, `value`,
substr('000' || year, -4) || '-' || substr('0' || month, -2) || '-' || substr('0' || day, -2) AS `date`,
strftime('%w', `date`) AS `weekday`
FROM
`foo`;
But this says
Error: no such column: date
This is an illustration. In real I have much more complex logic in calculating additional columns and need to reuse these calculated columns to calculate other columns and just copying whole code in every place I need its value look quite scary.
Is there a way I can address a computed column value in computing another one?
Use a subquery:
select f.*, strftime('%w', `date`) AS `weekday`
from (SELECT `year`, `month`, `day`, `hour`, `minute`, `value`,
substr('000' || year, -4) || '-' || substr('0' || month, -2) || '-' || substr('0' || day, -2) AS `date`
from foo
) t

Transposing a table through select query

I have a table like:
Key type value
---------------------
40 A 12.34
41 A 10.24
41 B 12.89
I want it in the format:
Types 40 41 42 (keys)
---------------------------------
A 12.34 10.24 XXX
B YYY 12.89 ZZZ
How can this be done through an SQL query. Case statements, decode??
What you're looking for is called a "pivot" (see also "Pivoting Operations" in the Oracle Database Data Warehousing Guide):
SELECT *
FROM tbl
PIVOT(SUM(value) FOR Key IN (40, 41, 42))
It was added to Oracle in 11g. Note that you need to specify the result columns (the values from the unpivoted column that become the pivoted column names) in the pivot clause. Any columns not specified in the pivot are implicitly grouped by. If you have columns in the original table that you don't wish to group by, select from a view or subquery, rather than from the table.
You can engage in a bit of wizardry and get Oracle to create the statement for you, so that you don't need to figure out what column values to pivot on. In 11g, when you know the column values are numeric:
SELECT
'SELECT * FROM tbl PIVOT(SUM(value) FOR Key IN ('
|| LISTAGG(Key, ',') WITHIN GROUP (ORDER BY Key)
|| ');'
FROM tbl;
If the column values might not be numeric:
SELECT
'SELECT * FROM tbl PIVOT(SUM(value) FOR Key IN (\''
|| LISTAGG(Key, '\',\'') WITHIN GROUP (ORDER BY Key)
|| '\'));'
FROM tbl;
LISTAGG probably repeats duplicates (would someone test this?), in which case you'd need:
SELECT
'SELECT * FROM tbl PIVOT(SUM(value) FOR Key IN (\''
|| LISTAGG(Key, '\',\'') WITHIN GROUP (ORDER BY Key)
|| '\'));'
FROM (SELECT DISTINCT Key FROM tbl);
You could go further, defining a function that takes a table name, aggregate expression and pivot column name that returns a pivot statement by first producing then evaluating the above statement. You could then define a procedure that takes the same arguments and produces the pivoted result. I don't have access to Oracle 11g to test it, but I believe it would look something like:
CREATE PACKAGE dynamic_pivot AS
-- creates a PIVOT statement dynamically
FUNCTION pivot_stmt (tbl_name IN varchar2(30),
pivot_col IN varchar2(30),
aggr IN varchar2(40),
quote_values IN BOOLEAN DEFAULT TRUE)
RETURN varchar2(300);
PRAGMA RESTRICT_REFERENCES (pivot_stmt, WNDS, RNPS);
-- creates & executes a PIVOT
PROCEDURE pivot_table (tbl_name IN varchar2(30),
pivot_col IN varchar2(30),
aggr IN varchar2(40),
quote_values IN BOOLEAN DEFAULT TRUE);
END dynamic_pivot;
CREATE PACKAGE BODY dynamic_pivot AS
FUNCTION pivot_stmt (
tbl_name IN varchar2(30),
pivot_col IN varchar2(30),
aggr_expr IN varchar2(40),
quote_values IN BOOLEAN DEFAULT TRUE
) RETURN varchar2(300)
IS
stmt VARCHAR2(400);
quote VARCHAR2(2) DEFAULT '';
BEGIN
IF quote_values THEN
quote := '\\\'';
END IF;
-- "\||" shows that you are still in the dynamic statement string
-- The input fields aren't sanitized, so this is vulnerable to injection
EXECUTE IMMEDIATE 'SELECT \'SELECT * FROM ' || tbl_name
|| ' PIVOT(' || aggr_expr || ' FOR ' || pivot_col
|| ' IN (' || quote || '\' \|| LISTAGG(' || pivot_col
|| ', \'' || quote || ',' || quote
|| '\') WITHIN GROUP (ORDER BY ' || pivot_col || ') \|| \'' || quote
|| '));\' FROM (SELECT DISTINCT ' || pivot_col || ' FROM ' || tbl_name || ');'
INTO stmt;
RETURN stmt;
END pivot_stmt;
PROCEDURE pivot_table (tbl_name IN varchar2(30), pivot_col IN varchar2(30), aggr_expr IN varchar2(40), quote_values IN BOOLEAN DEFAULT TRUE) IS
BEGIN
EXECUTE IMMEDIATE pivot_stmt(tbl_name, pivot_col, aggr_expr, quote_values);
END pivot_table;
END dynamic_pivot;
Note: the length of the tbl_name, pivot_col and aggr_expr parameters comes from the maximum table and column name length. Note also that the function is vulnerable to SQL injection.
In pre-11g, you can apply MySQL pivot statement generation techniques (which produces the type of query others have posted, based on explicitly defining a separate column for each pivot value).
Pivot does simplify things greatly. Before 11g however, you need to do this manually.
select
type,
sum(case when key = 40 then value end) as val_40,
sum(case when key = 41 then value end) as val_41,
sum(case when key = 42 then value end) as val_42
from my_table
group by type;
Never tried it but it seems at least Oracle 11 has a PIVOT clause
If you do not have access to 11g, you can utilize a string aggregation and a grouping method to approx. what you are looking for such as
with data as(
SELECT 40 KEY , 'A' TYPE , 12.34 VALUE FROM DUAL UNION
SELECT 41 KEY , 'A' TYPE , 10.24 VALUE FROM DUAL UNION
SELECT 41 KEY , 'B' TYPE , 12.89 VALUE FROM DUAL
)
select
TYPE ,
wm_concat(KEY) KEY ,
wm_concat(VALUE) VALUE
from data
GROUP BY TYPE;
type KEY VALUE
------ ------- -----------
A 40,41 12.34,10.24
B 41 12.89
This is based on wm_concat as shown here: http://www.oracle-base.com/articles/misc/StringAggregationTechniques.php
I'm going to leave this here just in case it helps, but I think PIVOT or MikeyByCrikey's answers would best suit your needs after re-looking at your sample results.

Troubleshooting PG Function

I have this function:
CREATE OR REPLACE FUNCTION CREATE_AIRSPACE_AVAILABILITY_RECORD
(cur_user VARCHAR, start_time VARCHAR, start_date VARCHAR, end_time VARCHAR, end_date VARCHAR, airspace_name VARCHAR)
RETURNS VOID AS '
DECLARE
c_user ALIAS for $1;
BEGIN
IF start_time IS NULL OR
start_date IS NULL OR
end_time IS NULL OR
end_date IS NULL THEN
INSERT INTO c_user.AIRSPACE_AVAILABILITY
(ASP_AIRSPACE_NM, ASA_TIME_ID, ASA_START_DT, ASA_END_DT)
SELECT airspace_name,
1,
ABP.ABP_START_DT,
ABP.ABP_STOP_DT
FROM ABP
WHERE EXISTS
(SELECT ASP.ASP_AIRSPACE_NM
FROM AIRSPACE ASP
WHERE ASP.ASP_AIRSPACE_NM = airspace_name);
ELSIF start_time IS NOT NULL AND
start_date IS NOT NULL AND
end_time IS NOT NULL AND
end_date IS NOT NULL THEN
INSERT INTO c_user.AIRSPACE_AVAILABILITY
(ASP_AIRSPACE_NM, ASA_TIME_ID, ASA_START_DT, ASA_END_DT)
SELECT airspace_name,
1,
TO_DATE(start_date||start_time,''YYMMDDHH24MI''),
TO_DATE(end_date||end_time,''YYMMDDHH24MI'')
FROM DUAL
WHERE EXISTS
(SELECT ASP.ASP_AIRSPACE_NM
FROM c_user.AIRSPACE ASP
WHERE ASP.ASP_AIRSPACE_NM = airspace_name);
END IF;
END ;
' LANGUAGE plpgsql;
I try calling it like so:
select * from CREATE_AIRSPACE_AVAILABILITY_RECORD('user1','','','','','');
and I get this error:
ERROR: schema "c_user" does not exist
SQL state: 3F000
Context: SQL statement "INSERT INTO c_user.AIRSPACE_AVAILABILITY (ASP_AIRSPACE_NM, ASA_TIME_ID, ASA_START_DT, ASA_END_DT) SELECT $1 , 1, TO_DATE( $2 || $3 ,'YYMMDDHH24MI'), TO_DATE( $4 || $5 ,'YYMMDDHH24MI') FROM DUAL WHERE EXISTS (SELECT ASP.ASP_AIRSPACE_NM FROM c_user.AIRSPACE ASP WHERE ASP.ASP_AIRSPACE_NM = $1 )"
PL/pgSQL function "create_airspace_availability_record" line 23 at SQL statement
Why isn't c_user being replaced with my param (user1)?
When you write something like this:
INSERT INTO c_user.AIRSPACE_AVAILABILITY
the database assumes that the 'c_user' is a namespace, not a variable. If you want to use the value of the c_user variable in such a query, you should use execute statement:
execute 'INSERT INTO ' || c_user || '.AIRSPACE_AVAILABILITY';
Why isn't c_user being replaced with my param (user1)?
Because the object names (metadata) cannot be substituted with the variables (data).
Choosing the metadata dynamically is usually not the best design, but if you need to live with it, you should do something along the lines of
EXECUTE 'INSERT INTO ' || c_user || '.AIRSPACE_AVAILABILITY ' …
instead.
See here for more details.