DB2 for i series: passing table row as parameter - db2

I've got a table with many columns:
CREATE TABLE TESTTABLE (ID INT GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1) PRIMARY KEY, F1 INT, F2 INT, F3 INT, F4 INT, F5 INT, F6 INT, F7 INT, F8 INT, F9 INT, F10 INT)
Then I've got a function that takes row-data and performs some type of operation. For the sake of this example, i just sum them up (not caring of null values).
CREATE OR REPLACE FUNCTION ROWFUNCTION(ID INT, F1 INT, F2 INT, F3 INT, F4 INT, F5 INT, F6 INT, F7 INT, F8 INT, F9 INT, F10 INT) RETURNS INT
LANGUAGE SQL
CONTAINS SQL
RETURN F1 + F2 + F3 + F4 + F5 + F6 + F7 + F8 + F9 + F10;
Then I've got some sort of function that calls this row-level function for each record.
CREATE OR REPLACE FUNCTION DOSOMETHING() RETURNS INT
LANGUAGE SQL
READS SQL DATA
BEGIN
DECLARE RES INT DEFAULT 0;
FOR C CURSOR FOR SELECT * FROM TESTTABLE DO
SET RES = RES + ROWFUNCTION(ID, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10);
END FOR;
RETURN RES;
END
Now the question is this. Having a function with a parameter for each table column is cumbersome.
It would be nice to have a 'record type' that just correspond to the table columns. This way I could pass a single parameter to the function. Maybe I missed something, but I couldn't find any solution to this problem in DB2 for i series.
If this solution is not possible, are there any workarounds?
I considered passing the ID, that allows the function to identify and load the record:
CREATE OR REPLACE FUNCTION ROWFUNCTION(ID INT) RETURNS INT
LANGUAGE SQL
READS SQL DATA
FOR SELECT * FROM TESTTABLE WHERE ID=ROWFUNCTION.ID DO
RETURN F1 + F2 + F3 + F4 + F5 + F6 + F7 + F8 + F9 + F10;
END FOR;
and the call would be:
...
SET RES = RES + ROWFUNCTION(ID);
...
Despite being almost exactly what i was looking for, this solution has the major drawback of generating a second access to the table for each record instead of using the data already retrieved by the main loop. The efficiency loss maybe negligible in most cases but in case we want also modify the records, having a double access to the table seems to add a unnecessary level of complexity. This is why I would welcome other solutions to this problem.

You may try the ARRAY data type, if you operate with just a set of integers.
CREATE TYPE INTARR AS INT ARRAY[]
CREATE OR REPLACE FUNCTION TEST_ARR (P_ARR INTARR)
RETURNS INT
BEGIN
RETURN (SELECT SUM (ID) FROM UNNEST (P_ARR) AS T (ID));
END
CREATE TABLE JUST_4_RES (A INT)
BEGIN
DECLARE V_RES INT DEFAULT 0;
DECLARE V_ARR INTARR;
FOR V_C AS
SELECT F1, F2, F3
FROM
(
VALUES
(1, 1, 1)
, (1, 1, 1)
) T (F1, F2, F3)
DO
SET V_ARR = ARRAY [V_C.F1, V_C.F2, V_C.F3];
SET V_RES = V_RES + TEST_ARR (V_ARR);
END FOR;
INSERT INTO JUST_4_RES (A) VALUES (V_RES);
END
SELECT * FROM JUST_4_RES
A
6
fiddle

Related

How to use ROW_NUMBER() - but instead of incremental numbers , generate A, B, C, D, E

I'm using ROW_NUMBER() to generate incremetal IDs numbers like this
ROW_NUMBER () OVER (ORDER BY ProductDate) as ID
i.e.
1
2
3
4
How can I do the same but create Alphabetic ordered letters like this
A
B
C
D
E
F.
..... AA, BB, CC
any ideas? Thx in advance
demo:db<>fiddle
You need to write an own function like this (original source):
CREATE OR REPLACE FUNCTION number_to_base(num BIGINT, base INTEGER)
RETURNS TEXT
LANGUAGE sql
IMMUTABLE
STRICT
AS $function$
WITH RECURSIVE n(i, n, r) AS (
SELECT -1, num - 1, 0
UNION ALL
SELECT i + 1, n / base, (n % base)::INT
FROM n
WHERE n > 0
)
SELECT string_agg(ch, '')
FROM (
SELECT chr(ascii('A') + r) ch
FROM n
WHERE i >= 0
ORDER BY i DESC
) ch
$function$;
It converts the numeral radix from base 10 to base 26 and replace the numbers 0-25 with A to Z. It's not quite perfect but a quick sketch (e.g. the A is 0 at the moment, you need to adjust it a bit).
You can use the the functions Chr() and Ascii() together to increment a character. The only other complication is determining when the next value is '...AA' instead a incrementing the last letter (i.e 'AY' => 'AZ' while 'AZ' => 'AAA). So try:
create or replace function alpha_sequence_next_val( alpha_seq_in text)
returns text
language sql
immutable
as $$
select case when alpha_seq_in is null
or alpha_seq_in = ''
then 'A'
when substr(alpha_seq_in, length(alpha_seq_in), 1) = 'Z'
then concat(substr(alpha_seq_in,1,length(alpha_seq_in)-1 ), 'AA')
else (substr(alpha_seq_in, 1,length(alpha_seq_in)-1)) ||
chr(ascii(substr(alpha_seq_in, length(alpha_seq_in)))+1)
end;
$$;
Test:
do $$
declare
seq_value text;
begin
for test_seq in 0 .. 26*3
loop
seq_value = alpha_sequence_next_val(seq_value);
raise notice 'Next Sequence==> %', seq_value;
end loop;
end;
$$;

Access pass-through query passing comma separated list as a parameter to SQL stored procedure

The SQL server is 2008. I have an Access 2016 front-end for reporting purposes. One report requires that one or more Product Classes from a list be chosen to report on. I have the VBA that creates the pass-through query with the appropriate single line:
exec dbo.uspINVDAYS 'A3,A4,A6,AA,AB'
I have this SQL code that should take the list as hard-coded here:
DECLARE #parProductClasses NVARCHAR(200) = 'A3,A4,A6,AA,AB';
DECLARE #ProductClasses NVARCHAR(200),#delimiter NVARCHAR(1) = ',';
SET #ProductClasses = #parProductClasses;
DECLARE #DAYS INT,#numDAYS int;
SET #DAYS = 395;
SET #numDAYS = #DAYS;
SELECT UPINVENTORY.StockCode, UPINVENTORY.[Description], UPINVENTORY.Supplier, UPINVENTORY.ProductClass
, UPINVENTORY.WarehouseToUse
, CAST(UPINVENTORY.Ebq AS INT)Ebq
, cast(UPINVENTORY.QtyOnHand AS INT)QtyOnHand
, cast(UPINVENTORY.PrevYearQtySold AS INT)PrevYearQtySold
, cast(UPINVENTORY.YtdQtyIssued AS INT)YtdQtyIssued
,#numDAYS as numDAYS
,CAST(ROUND((PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS,0) AS INT)TOTAL
,CASE WHEN (PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS
= 0 THEN 0
ELSE CAST(ROUND(QTYONHAND/((PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS),0)AS INT)
END FINAL
,CASE WHEN (PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS
= 0 THEN 0
ELSE CAST(ROUND(QTYONHAND/((PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS),0)AS INT)
END FINAL1
FROM
TablesCoE.dbo.vwRPUpInventory UPINVENTORY
WHERE UPINVENTORY.ProductClass IN (Select val From TablesCoE.dbo.split(#ProductClasses,','));
When I run this I get:
Msg 468, Level 16, State 9, Line 9
Cannot resolve the collation conflict between "SQL_Latin1_General_CP1_CI_AS" and "Latin1_General_BIN" in the equal to operation.
I cannot determine where
COLLATE SQL_Latin1_General_CP1_CI_AS
should go. Where am I equating or comparing? The SQL IN clause cannot handle the comma-separated list since it is not a strict SQL table.
Here's the code used to create the dbo.split() function:
CREATE FUNCTION dbo.split(
#delimited NVARCHAR(MAX),
#delimiter NVARCHAR(100)
) RETURNS #t TABLE (id INT IDENTITY(1,1), val NVARCHAR(MAX))
AS
BEGIN
DECLARE #xml XML
SET #xml = N'<t>' + REPLACE(#delimited,#delimiter,'</t><t>') + '</t>'
INSERT INTO #t(val)
SELECT r.value('.','varchar(MAX)') as item
FROM #xml.nodes('/t') as records(r)
RETURN
END
Thanks to Sandeep Mittal and I am sure others have very similar split functions. Run separately this function does operate as expected and provides a table of the comma-separated list objects.
DECLARE #parProductClasses NVARCHAR(200) = 'A3,A4,A6,AA,AB';
DECLARE #ProductClasses NVARCHAR(200),#delimiter NVARCHAR(1) = ',';
SET #ProductClasses = #parProductClasses;
Select val From TablesCoE.dbo.split(#ProductClasses,',')
Returns
val
A3
A4
A6
AA
AB
try this.
WHERE concat(',',#ProductClasses,',') like concat('%',UPINVENTORY.ProductClass,'%')
it's a silly way of checking if your productClass is within the #productClasses list.
After attempting to use a prefabricated table-valued variable versus on the fly in the WHERE clause, neither worked, I then started to try different placements of the COLLATE statement. I was complacent in applying COLLATE to the right-side with the collation listed on the left in the SQL error message. I tried the collation listed on the right of the SQL error message to the left side of the WHERE clause and the SQL code works to spec now. Here it is:
DECLARE #parProductClasses NVARCHAR(200) = 'A3,A4,A6,AA,AB';
DECLARE #ProductClasses NVARCHAR(200),#delimiter NVARCHAR(1) = ',';
SET #ProductClasses = #parProductClasses;
DECLARE #DAYS INT,#numDAYS int;
SET #DAYS = 395;
SET #numDAYS = #DAYS;
SELECT UPINVENTORY.StockCode, UPINVENTORY.[Description], UPINVENTORY.Supplier, UPINVENTORY.ProductClass
, UPINVENTORY.WarehouseToUse
, CAST(UPINVENTORY.Ebq AS INT)Ebq
, cast(UPINVENTORY.QtyOnHand AS INT)QtyOnHand
, cast(UPINVENTORY.PrevYearQtySold AS INT)PrevYearQtySold
, cast(UPINVENTORY.YtdQtyIssued AS INT)YtdQtyIssued
,#numDAYS as numDAYS
,CAST(ROUND((PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS,0) AS INT)TOTAL
,CASE WHEN (PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS
= 0 THEN 0
ELSE CAST(ROUND(QTYONHAND/((PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS),0)AS INT)
END FINAL
,CASE WHEN (PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS
= 0 THEN 0
ELSE CAST(ROUND(QTYONHAND/((PREVYEARQTYSOLD + YTDQTYISSUED)/#DAYS),0)AS INT)
END FINAL1
FROM
TablesCoE.dbo.vwRPUpInventory UPINVENTORY
WHERE UPINVENTORY.ProductClass COLLATE Latin1_General_BIN IN (SELECT val FROM TablesCoE.dbo.split(#ProductClasses,','));
Thanks for your suggestions #Krish and #Isaac.
Tim

PL/pgSQL: accessing fields of an element of an array of custom type

I've defined a custom type:
create type pg_temp.MYTYPE as (f1 int, f2 text);
Then, inside a function or a block, I've declared an array of such type:
declare ax MYTYPE[];
I can access an element through the familiar syntax ax[1].f1, but just for reading it.
When I use the same syntax for setting the field, I get a syntax error.
create type pg_temp.MYTYPE as (f1 int, f2 text);
do $$
declare x MYTYPE;
declare ax MYTYPE[];
declare f int;
begin
x.f1 = 10;
x.f2 = 'hello';
--assigning an element: OK
ax[1] = x;
--reading an element's field: OK
f = ax[1].f1;
--writing an elememt's field: SYNTAX ERROR:
ax[1].f1 = f;
end; $$
The error is the following:
psql:test.sql:28: ERROR: syntax error at or near "."
LINE 20: ax[1].f1 = f;
^
I've tried also the syntax (ax[1]).f1 with the same result.
Which is the correct syntax to do this?
Postgres server version: 9.2.2
PLpgSQL is sometimes very simple, maybe too simple. The left part of assignment statement is a example - on the left can be variable, record field, or array field only. Any complex left part expression are not supported.
You need a auxiliary variable of array element type.
DECLARE aux MTYPE;
aux := ax[1];
aux.f := 100000;
ax[1] := aux;
That seems particularly inconsequent of plpgsql since SQL itself can very well update a field of a composite type inside an array.
Demo:
CREATE TEMP TABLE mytype (f1 int, f2 text);
CREATE TEMP TABLE mycomp (c mytype);
INSERT INTO mycomp VALUES ('(10,hello)');
UPDATE mycomp
SET c.f1 = 12 -- note: without parentheses
WHERE (c).f1 = 10; -- note: with parentheses
TABLE mycomp;
c
------------
(12,hello)
CREATE TEMP TABLE mycomparr (ca mytype[]);
INSERT INTO mycomparr VALUES ('{"(10,hello)"}');
UPDATE mycomparr
SET ca[1].f1 = 12 -- works!
WHERE (ca[1]).f1 = 10;
TABLE mycomparr;
ca
----------------
{"(12,hello)"}
I wonder why plpgsql does not implement the same?
Be that as it may, another possible workaround would be to use UPDATE on a temporary table:
DO
$$
DECLARE
ax mytype[] := '{"(10,hello)"}';
BEGIN
CREATE TEMP TABLE tmp_ax ON COMMIT DROP AS SELECT ax;
UPDATE tmp_ax SET ax[1].f1 = 12;
-- WHERE (ax[1]).f1 = 10; -- not necessary while only 1 row.
SELECT t.ax INTO ax FROM tmp_ax t; -- table-qualify column!
RAISE NOTICE '%', ax;
END
$$;
That's more overhead than with #Pavel's simpler workaround. I would not use it for the simple case. But if you have lots of assignments it might still pay to use the temporary table.

Convert IP address in PostgreSQL to integer?

Is there a query that would be able to accomplish this?
For example given an entry '216.55.82.34' ..I would want to split the string by the '.'s, and apply the equation:
IP Number = 16777216*w + 65536*x + 256*y + z
where IP Address = w.x.y.z
Would this be possible from just a Query?
You can simply convert inet data-type to bigint: (inet_column - '0.0.0.0'::inet)
For example:
SELECT ('127.0.0.1'::inet - '0.0.0.0'::inet) as ip_integer
will output 2130706433, which is the integer representation of IP address 127.0.0.1
You can use split_part(). For example:
CREATE FUNCTION ip2int(text) RETURNS bigint AS $$
SELECT split_part($1,'.',1)::bigint*16777216 + split_part($1,'.',2)::bigint*65536 +
split_part($1,'.',3)::bigint*256 + split_part($1,'.',4)::bigint;
$$ LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT;
SELECT ip2int('200.233.1.2');
>> 3370713346
Or, if don't want to define a function, simply :
SELECT split_part(ip,'.',1)::bigint*16777216 + split_part(ip,'.',2)::bigint*65536 +
split_part(ip,'.',3)::bigint*256 + split_part(ip,'.',4)::bigint;
The drawback of the later is that, if the value is given by some computation instead of being just a table field, it can be inefficient to compute, or ugly to write.
PG 9.4
create or replace function ip2numeric(ip varchar) returns numeric AS
$$
DECLARE
ip_numeric numeric;
BEGIN
EXECUTE format('SELECT inet %L - %L', ip, '0.0.0.0') into ip_numeric;
return ip_numeric;
END;
$$ LANGUAGE plpgsql;
Usage
select ip2numeric('192.168.1.2');
$ 3232235778
create function dbo.fn_ipv4_to_int( p_ip text)
returns int
as $func$
select cast(case when cast( split_part(p_ip, '.', 1 ) as int ) >= 128
then
(
( 256 - cast(split_part(p_ip, '.', 1 ) as int ))
*
-power ( 2, 24 )
)
+ (cast( split_part(p_ip, '.', 2 ) as int ) * 65536 )
+ (cast( split_part(p_ip, '.', 3 ) as int ) * 256 )
+ (cast( split_part(p_ip, '.', 4 ) as int ) )
else (cast(split_part(p_ip, '.', 1 ) as int) * 16777216)
+ (cast(split_part(p_ip, '.', 2 ) as int) * 65536)
+ (cast(split_part(p_ip, '.', 3 ) as int) * 256)
+ (cast(split_part(p_ip, '.', 4 ) as int))
end as int )
$func$ LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT;
in case you need to get a 32 bit int. it'll return negative numbers for ips over 128.0.0.0. I'd use bigint if you can, but i had a case when i had the numbers stored as 32 bit numbers from another database.
Consider changing the column data type to inet, maybe is more efficient.
ALTER TABLE iptable ALTER COLUMN ip_from TYPE inet
USING '0.0.0.0'::inet + ip_from::bigint;
create index on iptable using gist (ip_from inet_ops);
Then to query
SELECT ip_from
FROM iptable
WHERE ip_from = '177.99.194.234'::inet

postgresql plpgsql compiler: why is it finding ambiguity where there is none

Why this runtime error?
ERROR: column reference "arrive" is ambiguous
LINE 6: case when ( (cast('05:00' as time) >= arrive)
DETAIL: It could refer to either a PL/pgSQL variable or a table column.
The thing is, the case statement is in a query, and I'm selecting from a table that does have a column called "arrive". I have not declared a variable called arrive. Why can't PG simply check to see that there's no such variable declared, and conclude that the reference must be to the column?
I am attached the code below. The only ostensible conflict I see is in the definition of the outgoing table returned by this function, in which there's a column called "arrive", and the use of the column name in the final select against the temporary table TT.
CREATE or replace function GetHourlyView
(v_whichDate date)
returns TABLE
(
stagecoach,
arrive time,
depart time,
t5am int,t6am int,t7am int,t8am int,t9am int,t10am int, t11am int,
t12pm int, t1pm int, t2pm int t3pm int,t4pm int,t5pm int,t6pm int,
t7pm int, t8pm int, t9pm int, t10pm int, t11pm int
)
as $body$
declare v_dow int := date_part('dow',v_whichDate);
begin
drop table if exists TT;
create temp table TT
(stagecoach varchar(25),
arrive time,
depart time,
t5am int,t6am int,t7am int,t8am int,t9am int,t10am int, t11am int,
t12pm int, t1pm int, t2pm int t3pm int,t4pm int,t5pm int,t6pm int,
t7pm int, t8pm int, t9pm int, t10pm int, t11pm int
) without OIDS on commit drop;
insert into TT
select * from
GetDailySchedule( v_whichDate);
-- (arrive=depart) means 'cancelled'
delete from TT where TT.arrive=TT.depart;
return QUERY
select
TT.stagecoach,
arrive,
depart,
case when ( (cast('05:00' as time) >= arrive) and (cast('05:00' as time) < depart )) then 1 else 0 end as t5am,
case when ( (cast('06:00' as time) >= arrive) and (cast('06:00' as time) < depart )) then 1 else 0 end as t6am,
<snip>
.
.
.
case when ( (cast('23:00' as time) >= arrive) and (cast('23:00' as time) < depart )) then 1 else 0 end as t11pm
from TT
;
drop table TT;
end
$body$
LANGUAGE 'plpgsql'
It's rather simple really: function parameters are visible everywhere inside the function body (except for dynamic SQL). That's true for all parameters: IN, OUT, INOUT, VARIADIC and any column name used in a RETURNS TABLE clause.
You also had a couple of other minor errors.
Avoid conflicts by table-qualifying column names: tt.arrive instead of just arrive.
Missing type for stagecoach in RETURNS TABLE.
Don't single-quote plpgsql at the end. It's an identifier.
I would also advise to adjust your syntax style. You are living in opposite world. The convention is to upper case SQL key words and lower case identifiers, not the other way round. Remember that PostgreSQL automatically casts unquoted identifiers to lower case.
All this aside, your function can be largely simplified to a plain SQL query. I wrapped it into an SQL function:
CREATE OR REPLACE FUNCTION gethourlyview(v_whichdate date)
RETURNS TABLE (
stagecoach text, arrive time, depart time
, t5am int, t6am int, t7am int, t8am int, t9am int, t10am int, t11am int
, t12pm int, t1pm int, t2pm int, t3pm int, t4pm int, t5pm int, t6pm int
, t7pm int, t8pm int, t9pm int, t10pm int, t11pm int
) AS
$body$
SELECT tt.stagecoach
,tt.arrive -- "depart" would cause conflict
,tt.depart
,CASE WHEN '05:00'::time >= tt.arrive
AND '05:00'::time < tt.depart THEN 1 ELSE 0 END -- AS t5am
,...
,CASE WHEN '23:00'::time >= tt.arrive
AND '23:00'::time < tt.depart THEN 1 ELSE 0 END -- AS t11pm
FROM getdailyschedule($1) tt
WHERE tt.arrive IS DISTINCT FROM tt.depart;
$body$ LANGUAGE sql;
No need to create a temporary table. You can do it all in one statement.