DB2 right padding with field length - db2

We have fields with varying lengths and want to right-pad them with spaces to the field length defined in the schema.
The following statement is working:
SELECT RPAD(field, LENGTH(field), ' ') AS field FROM schema.table;
This produces an SQL error 206 with SQLState 42703: is not valid in the context where it is used.
// Our application resolves the prepared statement's ? - this is working fine
INSERT INTO schema.table (field) VALUES (RPAD(?, LENGTH(field), ' '));
The same happens with:
INSERT INTO schema.table (field) VALUES (RPAD(?, LENGTH(schema.table.field), ' '));
Is there any possibility to avoid hardcoding the field length?

Your problem is that scalar functions operate on rows; LENGTH(field) only works within a statement that returns rows, such as a select statement. To understand why, imagine putting some other function in place of LENGTH(). LCASE(field), for example, takes the lowercase of the string in a particular row. It wouldn't make sense applied generically to a column. Even LENGTH() can vary row-by-row in some cases: if the column is of type VARCHAR, LENGTH() returns the length of the actual string.
The solution is to select any row, perform the LENGTH() operation on the field, and store the result in a variable:
CREATE OR REPLACE VARIABLE field_length INTEGER;
SET field_length = (
SELECT LENGTH(field) FROM schema.table
WHERE field IS NOT NULL
FETCH FIRST ROW ONLY
);
You only need to do this once in your code. Then, whenever you need to use the length:
INSERT INTO schema.table (field) VALUES (RPAD(?, field_length, ' '));
Note that this solution depends on field being defined as a CHAR(x) rather than a VARCHAR(x). If you had to do this with a VARCHAR, you could find out the length of the field from the syscat.columns system table.
EDIT: added handling of null values since LENGTH() could return null if the value in field is null.

If you want a fixed length column, why are you using VARCHAR? Use CHAR - DB2 will automatically pad the values for you.

Related

Scalar-valued function does not return NULL but a 'NULL' string

I need to import data from Excel into a ms sql database and I thought using the OPENROWSET would be a good idea... well, it is not bad but has some side effects.
The data I'm receiving is not allways 100% correct. By correct I mean cells that should be NULL (and in Excel empty) sometimes contain the string "NULL" or some other junk like whitespaces. I tried to fix it with this script:
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER FUNCTION [dbo].[NullIfEmpty](#input nvarchar)
RETURNS nvarchar(max)
AS
BEGIN
if (#input = '' or #input = 'NULL')
begin
return NULL
end
return #input
END
But strange things happen. This gives me a string with the text "NULL" instead of a real NULL so the grid cell after querying the database isn't yellow but contains normal text even though the target column allows NULL.
A simple test with:
select dbo.nullifempty('NULL')
or
select dbo.nullifempty(null)
also yields a string.
Do you know why this is happening and how I can fix it?
To get null for empty strings or strings that are the word NULL, you could just use coalesce and nullif:
COALESCE(NULLIF(#input, 'NULL'), NULLIF(#Input, ''), #input)
Please note that the problem in your original code is because you didn't specify the length of the #input parameter - so SQL Server created it as varchar(1).
You should always specify length for char/varchar/nchar and nvarchar.
From nchar and nvarchar page remarks:
When n is not specified in a data definition or variable declaration statement, the default length is 1. When n is not specified with the CAST function, the default length is 30.
(n referring to the n in nchar(n) or nvarchar(n))
repleace lines with 'ALTER"
ALTER FUNCTION [dbo].[NullIfEmpty](#input nvarchar(max))
and with line with 'if'
if (LTRIM(RTRIM(#input)) = '' or #input IS NULL)
you should reassign value for declared variable using set
''''
BEGIN
if (#input = '' or #input = 'NULL')
begin
set #input = NULL
end
select #input
END
test

String Manipulation throws error while Inserting into PostgreSql Jsonb Column

I have the following code, to insert data into my table res(ID bigserial,
Results jsonb not null). I want to insert data so that 'Display' column always has the 'i' appended to it, so that every row has a different value for 'Display' Column.
DO $$
declare cnt bigint;
BEGIN
FOR i IN 1..2 LOOP
INSERT INTO res (Results)
VALUES ('{"cid":"CID1","Display":"User One'|| i || '","FName":"Userfff","LName":"One"}');
INSERT INTO res (Results)
VALUES ('{"cid":"CID1","Display":"User One'|| i || '","FName":"Userfff","LName":"One"}');
INSERT INTO res (Results)
VALUES ('{"cid":"CID1","Display":"User One'|| i || '","FName":"Userfff","LName":"One"}');
END LOOP;
END;
$$
LANGUAGE plpgsql;
However, when I run this code, I get the following error:
ERROR: column "results" is of type jsonb but expression is of type text
LINE 2: VALUES ('{"cid":"CID1","Display":"User One'|| i ...
^
HINT: You will need to rewrite or cast the expression.
How should I modify my query so that the query runs successfully?
No need for a loop or even PL/pgSQL.
You can generate any number of rows using generate_series()
To put a value into a string I prefer to use format() as that makes dealing with strings a lot easier:
insert into res(results)
select format('{"cid":"CID1","Display":"User One %s","FName":"Userfff","LName":"One"}', i::text)::jsonb
from generate_series(1,5) g(i);
The second parameter to the format() function will be put where the %s in the first parameter.
The above inserts 5 rows each with a different value for display. This is what you stated in your question. Your sample code however inserts a total of 6 rows where 3 rows have the same value for display.

How to derive a column name in the return type from input parameters to the function?

Using Postgres 9.5 I have built this function:
CREATE or REPLACE FUNCTION func_getratio_laglag(_numeratorLAG text, _n1 int, _denominatorLAG text, _n2 int, _table text)
RETURNS TABLE (date_t timestamp without time zone, customer_code text, index text, ratio real) AS
$BODY$
BEGIN
RETURN QUERY EXECUTE
'SELECT
date_t,
customer_code,
index,
(LAG('||quote_ident(_numeratorLAG)||',' || quote_literal(_n1)||') OVER W / LAG('||quote_ident(_denominatorLAG)||','|| quote_literal(_n2)||') OVER W) '
|| ' FROM ' || quote_ident(_table)
|| ' WINDOW W AS (PARTITION BY customer_code ORDER BY date_t asc);';
END;
$BODY$ LANGUAGE plpgsql;
All the function does is allow me the ability to pick a 2 different columns from a specified table and calculate a ratio between them based on different lag windows. To execute the function above I use the following query:
SELECT * FROM func_getratio_laglag('order_first',1,'order_last',0,'customers_hist');
This returns a table with the column labels date_t, customer_code, index and ratio. I have really struggled on how to output ratio as a dynamic column label. That is, I would like to make it contingent on the input parameters e.g. if I ran the select query above then I would like the column labels date_t, customer_code, index and order_first_1_order_last_0.
I am stuck, any advice or hints?
How to derive a column name in the return type from input parameters to the function?
The short answer: Not possible.
SQL is very rigid about column data types and names. Those have to be declared before or at call time at the latest. No exceptions. No truly dynamic column names.
I can think of 3 half-way workarounds:
1. Column aliases
Use your function as is (or rather the audited version I suggest below) and add column aliases in the function call:
SELECT * FROM func_getratio_laglag('order_first',1,'order_last',0,'customers_hist')
AS f(date_t, customer_code, index, order_first_1_order_last_0)
I would do that.
2. Column definition list
Create your function to return anonymous records:
RETURNS SETOF record
Then you have to provide a column definition list with every call:
SELECT * FROM func_getratio_laglag('order_first',1,'order_last',0,'customers_hist')
AS f(date_t timestamp, customer_code text, index text, order_first_1_order_last_0 real)
I would not do that.
3. Use a registered row type as polymorphic input / output type.
Mostly useful if you happen to have row types at hand. You could register a row type on the fly by crating a temporary table, but that seems like overkill for your use case.
Details in the last chapter of this answer:
Refactor a PL/pgSQL function to return the output of various SELECT queries
Function audit
Use format() to make building query string much more safe and simple.
Read the manual if you are not familiar with it.
CREATE OR REPLACE FUNCTION func_getratio_laglag(
_numerator_lag text, _n1 int
, _denominator_lag text, _n2 int
, _table regclass)
RETURNS TABLE (date_t timestamp, customer_code text, index text, ratio real) AS
$func$
BEGIN
RETURN QUERY EXECUTE format (
'SELECT date_t, customer_code, index
, (lag(%I, %s) OVER w / lag(%I, %s) OVER w) -- data type must match
FROM %s
WINDOW w AS (PARTITION BY customer_code ORDER BY date_t)'
, _numerator_lag, _n1, _denominator_lag, _n2, _table::text
);
END
$func$ LANGUAGE plpgsql;
Note the data type regclass for the table name. That's my personal (optional) suggestion.
Table name as a PostgreSQL function parameter
Aside: I would also advise not to use mixed-case identifiers in Postgres.
Are PostgreSQL column names case-sensitive?

PostgreSQL - Inserting a combination of values into a table (Example Included)

For example I calculate a predetermined value which is stored in value. I would like to insert the value into the table as A100.
I tried doing this at first: insert into t values('A'+value);
This didn't seem to work. Does anyone know how I might be able to do this?
You can use concat function to do this:
insert into t values(concat('A',value::character varying));
You can use concat or ||
insert into t values(concat('A', value));
insert into t values('A' || value);
In both cases You can, but You don't need cast int to character varying, because concat:
Concatenate the text representations of all the arguments
|| cast non-string arguments to text.

PostgreSQL - set a default cell value according to another cell value

If i have a column say column a of any given values, and i want another column column b to have a default value according to the value of column a
In another words:
if column a = 'peter' then column b default value = 'doctor'.
This is not possible with a simple DEFAULT value, as the manual clearly states:
The value is any variable-free expression (subqueries and
cross-references to other columns in the current table are not allowed).
You could use a trigger instead:
CREATE OR REPLACE FUNCTION trg_foo_b_default()
RETURNS trigger
LANGUAGE plpgsql AS
$func$
BEGIN
-- For just a few constant options, CASE does the job:
NEW.b := CASE NEW.a
WHEN 'peter' THEN 'doctor'
WHEN 'weirdo' THEN 'shrink'
WHEN 'django' THEN 'undertaker'
-- ELSE null default
END;
/*
-- For more, or dynamic options, consider a lookup table:
SELECT INTO NEW.b t.b
FROM def_tbl t
WHERE t.a = NEW.a;
*/
RETURN NEW;
END
$func$;
CREATE TRIGGER b_default
BEFORE INSERT ON foo
FOR EACH ROW
WHEN (NEW.b IS NULL AND NEW.a IS NOT NULL)
EXECUTE FUNCTION trg_foo_b_default();
For Postgres 10 or older use the EXECUTE PROCEDURE ... instead. See:
Trigger function does not exist, but I am pretty sure it does
To make it more efficient use a WHEN clause in the trigger definition (available since Postgres 9.0). This way the trigger function is only executed when it's actually useful. (Assuming we can let b IS NULL slide if a IS NULL.)
In Postgres 12 or later, a GENERATED column may be the better solution. See jian's added answer. Note, however, these restrictions in the manual:
The generation expression can refer to other columns in the table, but
not other generated columns. Any functions and operators used must be
immutable. References to other tables are not allowed.
This trigger is subtly different from a DEFAULT value in that null in b is always replaced with the value derived from a, while a DEFAULT is just the default and can be overruled with any explicit input.
A GENERATED column does not allow input to begin with.
In PostgreSQL 12 or later we can use Generated Columns.
https://www.postgresql.org/docs/12/ddl-generated-columns.html
example:
create temp table foo (a text ,
b text GENERATED ALWAYS AS (
case when a = 'telegram' then 'im'
WHEN a = 'proton' THEN 'email'
WHEN a = 'infinity' THEN 'idea'
else 'bad idea'
end
) stored);
--Test time.
insert into foo(a) values ('infinity');
insert into foo(a) values ('infinity1');
returns;
a | b
-----------+----------
infinity1 | bad idea
infinity | idea
When you try to insert into foo(b) values ('infinity1') yield Errors.
--ERROR: cannot insert into column "b" DETAIL: Column "b" is a generated column.