SAS to PostgreSQL(PADB) code - summing field if they exists - postgresql

I'm having a challenge with a piece of code from SAS that I need to convert to SQL.
Usually I'm very good at this but right not I'm facing a new challenge and so far all my ideas to resolve it are failing and I'm not finding the right way to do so.
I need to be able to pick up field dynamically for this request, like if a field has a certain pattern in it's name I need to sum those fields.
my version of PostgreSQL is 8.0.2, PADB 5.3.3.1 78560
So the table may or may not have a field like bas_txn_03cibc_vcl.
I wrote a function that should output ' ' as bas_txn_03cibc_vcl when the field is not found in the information_schema table and use bas_txn_03cibc_vcl if found.
But when I execute the command I get the error that UDF cannot be used on PADB tables.
"ERROR: XX000: User-defined SQL language function "check_if_field_exists(character varying,character varying,character varying)" cannot be used in a query that references PADB tables."
Right now I'm building a new approach using stored procedure but it will limit the use case. Any other idea on how I can select field dynamically?
Function:
CREATE OR REPLACE FUNCTION check_if_field_exists(_schm text, _tbl text, _field text)
RETURNS text AS
$BODY$
DECLARE
_output_ text:= '' as _field;
BEGIN
EXECUTE 'SELECT column_name into : _output_ FROM rdwaeprd.information_schema.columns
where table_schema='''|| _schm||'''
and table_name='''|| _tbl||'''
and column_name='''|| _field||'''
order by table_name,column_name;';
RETURN _output_;
END
$BODY$
LANGUAGE PLPGSQL;
and then I would use it like this
select indiv_id,ae_psamson.check_if_field_exists('ae_psamson','activ_cc', 'tot_txn_03AMX_AMXE') ,tot_txn_03AMX_AMXD
from activ_cc
group by indiv_id,tot_txn_03AMX_AMXD;
Where the function would either return '' as tot_txn_03AMX_AMXE or simply, tot_txn_03AMX_AMXE.... the idea is to make the query not return an error if the field does not exists.
Like I said I need a new function or approach as this one is not working...

I managed to make a function that make it work!
Basically one of the issue what that information schema was using unsupported function in UDF.
This solution works fine:
CREATE OR REPLACE FUNCTION check_if_field_exists(_schm text, _tbl text, _field text)
RETURNS varchar(55) AS
$BODY$
DECLARE
_output_ varchar(55) :=' 0 as '|| _field;
-- name := (SELECT t.name from test_table t where t.id = x);
BEGIN
EXECUTE 'drop table if exists col_name';
EXECUTE 'create table col_name as SELECT att.attname::character varying(128) AS colname
FROM pg_class cl, pg_namespace ns, pg_attribute att
WHERE cl.relnamespace = ns.oid AND cl.oid = att.attrelid AND ns.nspname='''|| _schm ||'''
and cl.relname='''|| _tbl ||'''
and colname like '''|| _field||''''; -- INTO _output_;
select colname from col_name into _output_ ;
if _output_ is null then
_output_ :=' 0 as '|| _field;
end if;
RETURN _output_ ;
END
$BODY$
LANGUAGE PLPGSQL;

Related

How do I bind a variable in postgres functions

Here's my code:
CREATE OR REPLACE FUNCTION public.view_columns_f(viewname text)
RETURNS TABLE(columnn_name text, data_type text)
LANGUAGE plpgsql
AS $function$
BEGIN
return query execute
$$SELECT attname, format_type(atttypid, atttypmod) AS data_type
FROM pg_attribute
WHERE attrelid = '$1'::regclass$$ using viewname ;
END;
The error is relation "$1" doesn't exist, because I'm not binding it correctly.
Adrian pointed out a couple of problems, I fixed a couple more:
CREATE OR REPLACE FUNCTION public.view_columns_f(viewname regclass)
RETURNS TABLE (columnn_name name, data_type text)
LANGUAGE plpgsql AS
$func$
BEGIN
RETURN QUERY
SELECT attname, format_type(atttypid, atttypmod) -- AS data_type
FROM pg_attribute
WHERE attrelid = $1
AND NOT attisdropped -- exclude deleted columns
AND attnum > 0 -- exclude internal system columns
ORDER BY attnum; -- original order
END
$func$;
Call:
SELECT * FROM public.view_columns_f('my_view');
Most importantly, you don't need dynamic SQL at all, luckily. Get a grip on plain PL/pgSQL first, before playing with trickier dynamic SQL.
Could be a simpler SQL function, nothing requires PL/pgSQL.
The function name is misleading. You get columns for any registered relation this way, not just for a view.
Further reading:
How to list the columns of a view in Postgres?
Table name as a PostgreSQL function parameter

PostgreSQL Function to dynamically reshape and create tables in loop function

I am pretty fresh to PostgreSQL, so please be kind.
I am pretty sure that my problem is that I am mixing plain and dynamic SQL. I have read the relevant documentation but I am not experienced enough to see where I have gone wrong (hoping that my issue is not something more fundamental).
Currently the script is failing with a Query execution error:
SQL Error [42601]: ERROR: syntax error at or near "CREATE"
I intend to use this function to unpivot >9,000 tables (for analysis purposes); fortunately all tables have the same structure.
CREATE OR REPLACE FUNCTION all_schemaTables_unpivot(_schemaName text, _tableName text)
RETURNS void AS
$BODY$
DECLARE
_tbl record;
BEGIN
FOR _tbl IN
SELECT
quote_ident(schemaname) || '.' || quote_ident(tablename) AS fName,
quote_ident(tablename) AS tName
FROM pg_tables
WHERE schemaname = _schemaName
AND tablename LIKE _tableName
LOOP
EXECUTE 'CREATE TABLE ' || _tbl.tName || '_up AS
SELECT region_id, key AS sequential_id, value
FROM (SELECT row_to_json(t.*) AS line, region_id
FROM ' || _tbl.tName || ' AS t) AS r
JOIN LATERAL json_each_text(r.line) ON (key <> "region_id")';
END LOOP;
END;
$BODY$ LANGUAGE plpgsql;
Thanks in advance.
To get the script to work I just needed to fix two things:
I originally did not set appropriate spaces around the query concatenating (thanks #KaushikNayak)
I incorrectly set the quoting values around my dynamic variables (see the 'quote_ident(_tbl.newTableName)' addition. People more expert in PostgreSQL than I will surely have a much cleaner approach - but at least it works!
Moral of the story, sometimes the fix is staring you in the face, but you have been staring at the script for too long! Leave it for awhile and the answer becomes clear.
CREATE OR REPLACE FUNCTION
all_schemaTables_unpivot(_schemaName text, _tableName text)
RETURNS void AS
$BODY$
DECLARE
_tbl record;
BEGIN
FOR _tbl IN
SELECT
quote_ident(schemaname) || '.' || quote_ident(tablename) AS fullNamePath,
quote_ident(tablename) || '_up' AS newTableName
FROM pg_tables
WHERE schemaname = _schemaName
AND tablename LIKE _tableName
LOOP
EXECUTE 'CREATE TABLE '|| quote_ident(_tbl.newTableName) ||' AS
SELECT region_id, key AS sequential_id, value
FROM (SELECT row_to_json(t.*) AS line, region_id
FROM '|| _tbl.fullNamePath ||' AS t) AS r
JOIN LATERAL json_each_text(r.line) ON (key <> "region_id");';
END LOOP;
END;
$BODY$ LANGUAGE plpgsql;

How to dynamically copy tables from information schema inside a trigger function

I have an insert trigger function in which NEW.schema_name references a schema. I want to dynamically copy the tables found inside that schema ('foobaz','barbaz') as 'foo' and 'bar'. I then can perform queries without dynamic sql.
How can I create a function or simply copy/paste the same block of code to achive that.
EDIT :
I cannot get that dynamic query to work.
The part inside the WITH statement is working.
Not the bottom 'execute' part. I do not know if it is a syntax problem, or bad cast or whatever constraint there is in pgsql that makes it not working.
WITH info_schema_subset_table as (SELECT table_schema, table_name,
array_to_string((regexp_split_to_array(table_name,'_'))[4:array_length(regexp_split_to_array(table_name,'_'),1)-1] as new_table
FROM information_schema.tables
where table_schema = "schema_searched"
ORDER BY new_table ASC)
EXECUTE 'CREATE TABLE $2 as (SELECT * FROM $1)'
USING info_schema_subset_table.table_schema || '.' ||info_schema_subset_table.table_name,info_schema_subset_table.new_table;
EDIT 2
... Broken code removed...
In the code below, in which I'm unsure if the syntax is right, I get the following from the trigger
Provider errors:
PostGIS error while adding features: ERREUR: l'opérateur n'existe pas : record ~~ unknown
LINE 1: SELECT old_table LIKE '%ens%'
^
HINT: Aucun opérateur ne correspond au nom donné et aux types d'arguments.
Vous devez ajouter des conversions explicites de type.
QUERY: SELECT old_table LIKE '%ens%'
CONTEXT: fonction PL/pgsql validation_sio.afi_validation_sio(), ligne 18 à CASE
EDIT 3 :
CREATE OR REPLACE FUNCTION foo.foo()
RETURNS TRIGGER AS
$BODY$
DECLARE
old_table record;
new_table record;
dynamic_query text;
BEGIN
IF TG_OP = 'INSERT'
THEN
FOR old_table IN SELECT table_schema|| '.' ||table_name
FROM information_schema.tables
where table_schema = NEW.nom_schema
LOOP
CASE
WHEN
old_table LIKE '%ens%' THEN
new_table := concat('SIT_',array_to_string((regexp_split_to_array(info_schema.old_table,'_'))[4:array_length(regexp_split_to_array(info_schema.old_table,'_'),1)-1],'_'));
ELSE
new_table := concat('SID_',array_to_string((regexp_split_to_array(info_schema.old_table,'_'))[4:array_length(regexp_split_to_array(info_schema.old_table,'_'),1)-1],'_'));
END CASE;
dynamic_query := format('SELECT * FROM' || old_table ||);
EXECUTE dynamic_query
INTO new_table;
END LOOP;
RETURN NEW;
END IF;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
CREATE TRIGGER foo
AFTER INSERT ON validation.validationfoo
FOR EACH ROW EXECUTE PROCEDURE foo.foo();
I've reformatted your trigger function a bit and changed a few things, see if this works.
CREATE OR REPLACE FUNCTION foo.foo()
RETURNS TRIGGER AS
$BODY$
DECLARE
old_table record;
new_table record;
dynamic_query text;
BEGIN
IF TG_OP = 'INSERT' THEN
FOR old_table IN
SELECT table_schema || '.' || table_name AS old_table_name
FROM information_schema.tables
WHERE table_schema = NEW.nom_schema
LOOP
new_table := concat(CASE WHEN old_table.old_table_name LIKE '%ens%' THEN 'SIT_' ELSE 'SID_' END,array_to_string((regexp_split_to_array(info_schema.old_table,'_'))[4:array_length(regexp_split_to_array(info_schema.old_table,'_'),1)-1],'_'));
dynamic_query := 'CREATE TABLE ' || new_table || ' AS SELECT * FROM ' || old_table.old_table_name;
EXECUTE dynamic_query;
END LOOP;
RETURN NEW;
END IF;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
So the main things:
old_table is a record, so your comparison of it to a string with LIKE was failing. You need to use the field name. So I gave your field a name, and used that field name in the LIKE comparison.
Changed the new_table assignment to put the CASE statement only on the one item that changes, to make the difference more obvious and the code more concise. Mind you, I don't know if the rest of that line is actually valid, I just left it as is.
Changed the creation dynamic_query. As I said in the comment, the format function was being used incorrectly, so I just went with standard string concatenation instead.
Changed dynamic_query's SQL to what I think you actually want it to do. You want it to copy the content of the table to a new table, right? So that will do it.
You cannot have EXECUTE inside an SQL statement, it is a PL/pgSQL statement.
Loop through the tables and issue one EXECUTE for each.
Mind that you cannot have a schema or table name as a parameter with USING, because these names need to be known at parse time.
Use the format function to construct your dynamic statement so you can avoid SQL injection by users who maliciously create tables with weird names.

Function to return dynamic set of columns for given table

I have a fields table to store column information for other tables:
CREATE TABLE public.fields (
schema_name varchar(100),
table_name varchar(100),
column_text varchar(100),
column_name varchar(100),
column_type varchar(100) default 'varchar(100)',
column_visible boolean
);
And I'd like to create a function to fetch data for a specific table.
Just tried sth like this:
create or replace function public.get_table(schema_name text,
table_name text,
active boolean default true)
returns setof record as $$
declare
entity_name text default schema_name || '.' || table_name;
r record;
begin
for r in EXECUTE 'select * from ' || entity_name loop
return next r;
end loop;
return;
end
$$
language plpgsql;
With this function I have to specify columns when I call it!
select * from public.get_table('public', 'users') as dept(id int, uname text);
I want to pass schema_name and table_name as parameters to function and get record list, according to column_visible field in public.fields table.
Solution for the simple case
As explained in the referenced answers below, you can use registered (row) types, and thus implicitly declare the return type of a polymorphic function:
CREATE OR REPLACE FUNCTION public.get_table(_tbl_type anyelement)
RETURNS SETOF anyelement AS
$func$
BEGIN
RETURN QUERY EXECUTE format('TABLE %s', pg_typeof(_tbl_type));
END
$func$ LANGUAGE plpgsql;
Call:
SELECT * FROM public.get_table(NULL::public.users); -- note the syntax!
Returns the complete table (with all user columns).
Wait! How?
Detailed explanation in this related answer, chapter
"Various complete table types":
Refactor a PL/pgSQL function to return the output of various SELECT queries
TABLE foo is just short for SELECT * FROM foo:
Is there a shortcut for SELECT * FROM?
2 steps for completely dynamic return type
But what you are trying to do is strictly impossible in a single SQL command.
I want to pass schema_name and table_name as parameters to function and get record list, according to column_visible field in
public.fields table.
There is no direct way to return an arbitrary selection of columns (return type not known at call time) from a function - or any SQL command. SQL demands to know number, names and types of resulting columns at call time. More in the 2nd chapter of this related answer:
How do I generate a pivoted CROSS JOIN where the resulting table definition is unknown?
There are various workarounds. You could wrap the result in one of the standard document types (json, jsonb, hstore, xml).
Or you generate the query with one function call and execute the result with the next:
CREATE OR REPLACE FUNCTION public.generate_get_table(_schema_name text, _table_name text)
RETURNS text AS
$func$
SELECT format('SELECT %s FROM %I.%I'
, string_agg(quote_ident(column_name), ', ')
, schema_name
, table_name)
FROM fields
WHERE column_visible
AND schema_name = _schema_name
AND table_name = _table_name
GROUP BY schema_name, table_name
ORDER BY schema_name, table_name;
$func$ LANGUAGE sql;
Call:
SELECT public.generate_get_table('public', 'users');
This create a query of the form:
SELECT usr_id, usr FROM public.users;
Execute it in the 2nd step. (You might want to add column numbers and order columns.)
Or append \gexec in psql to execute the return value immediately. See:
How to force evaluation of subquery before joining / pushing down to foreign server
Be sure to defend against SQL injection:
INSERT with dynamic table name in trigger function
Define table and column names as arguments in a plpgsql function?
Asides
varchar(100) does not make much sense for identifiers, which are limited to 63 characters in standard Postgres:
Maximum characters in labels (table names, columns etc)
If you understand how the object identifier type regclass works, you might replace schema and table name with a singe regclass column.
I think you just need another query to get the list of columns you want.
Maybe something like (this is untested):
create or replace function public.get_table(_schema_name text, _table_name text, active boolean default true) returns setof record as $$
declare
entity_name text default schema_name || '.' || table_name;
r record;
columns varchar;
begin
-- Get the list of columns
SELECT string_agg(column_name, ', ')
INTO columns
FROM public.fields
WHERE fields.schema_name = _schema_name
AND fields.table_name = _table_name
AND fields.column_visible = TRUE;
-- Return rows from the specified table
RETURN QUERY EXECUTE 'select ' || columns || ' from ' || entity_name;
RETURN;
end
$$
language plpgsql;
Keep in mind that column/table references may need to be surrounded by double quotes if they have certain characters in them.

How to add column if not exists on PostgreSQL?

Question is simple. How to add column x to table y, but only when x column doesn't exist ? I found only solution here how to check if column exists.
SELECT column_name
FROM information_schema.columns
WHERE table_name='x' and column_name='y';
With Postgres 9.6 this can be done using the option if not exists
ALTER TABLE table_name ADD COLUMN IF NOT EXISTS column_name INTEGER;
Here's a short-and-sweet version using the "DO" statement:
DO $$
BEGIN
BEGIN
ALTER TABLE <table_name> ADD COLUMN <column_name> <column_type>;
EXCEPTION
WHEN duplicate_column THEN RAISE NOTICE 'column <column_name> already exists in <table_name>.';
END;
END;
$$
You can't pass these as parameters, you'll need to do variable substitution in the string on the client side, but this is a self contained query that only emits a message if the column already exists, adds if it doesn't and will continue to fail on other errors (like an invalid data type).
I don't recommend doing ANY of these methods if these are random strings coming from external sources. No matter what method you use (client-side or server-side dynamic strings executed as queries), it would be a recipe for disaster as it opens you to SQL injection attacks.
Postgres 9.6 added ALTER TABLE tbl ADD COLUMN IF NOT EXISTS column_name.
So this is mostly outdated now. You might use it in older versions, or a variation to check for more than just the column name.
CREATE OR REPLACE function f_add_col(_tbl regclass, _col text, _type regtype)
RETURNS bool
LANGUAGE plpgsql AS
$func$
BEGIN
IF EXISTS (SELECT FROM pg_attribute
WHERE attrelid = _tbl
AND attname = _col
AND NOT attisdropped) THEN
RETURN false;
ELSE
EXECUTE format('ALTER TABLE %s ADD COLUMN %I %s', _tbl, _col, _type);
RETURN true;
END IF;
END
$func$;
Call:
SELECT f_add_col('public.kat', 'pfad1', 'int');
Returns true on success, else false (column already exists).
Raises an exception for invalid table or type name.
Why another version?
This could be done with a DO statement, but DO statements cannot return anything. And if it's for repeated use, I would create a function.
I use the object identifier types regclass and regtype for _tbl and _type which a) prevents SQL injection and b) checks validity of both immediately (cheapest possible way). The column name _col has still to be sanitized for EXECUTE with quote_ident(). See:
Table name as a PostgreSQL function parameter
format() requires Postgres 9.1+. For older versions concatenate manually:
EXECUTE 'ALTER TABLE ' || _tbl || ' ADD COLUMN ' || quote_ident(_col) || ' ' || _type;
You can schema-qualify your table name, but you don't have to.
You can double-quote the identifiers in the function call to preserve camel-case and reserved words (but you shouldn't use any of this anyway).
I query pg_catalog instead of the information_schema. Detailed explanation:
How to check if a table exists in a given schema
Blocks containing an EXCEPTION clause are substantially slower.
This is simpler and faster. The manual:
Tip
A block containing an EXCEPTION clause is significantly more
expensive to enter and exit than a block without one.
Therefore, don't use EXCEPTION without need.
Following select query will return true/false, using EXISTS() function.
EXISTS(): The argument of EXISTS is an arbitrary SELECT statement, or
subquery. The subquery is evaluated to determine whether it returns
any rows. If it returns at least one row, the result of EXISTS is
"true"; if the subquery returns no rows, the result of EXISTS is
"false"
SELECT EXISTS(SELECT column_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'x'
AND column_name = 'y');
and use the following dynamic SQL statement to alter your table
DO
$$
BEGIN
IF NOT EXISTS (SELECT column_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'x'
AND column_name = 'y') THEN
ALTER TABLE x ADD COLUMN y int DEFAULT NULL;
ELSE
RAISE NOTICE 'Already exists';
END IF;
END
$$
For those who use Postgre 9.5+(I believe most of you do), there is a quite simple and clean solution
ALTER TABLE if exists <tablename> add if not exists <columnname> <columntype>
the below function will check the column if exist return appropriate message else it will add the column to the table.
create or replace function addcol(schemaname varchar, tablename varchar, colname varchar, coltype varchar)
returns varchar
language 'plpgsql'
as
$$
declare
col_name varchar ;
begin
execute 'select column_name from information_schema.columns where table_schema = ' ||
quote_literal(schemaname)||' and table_name='|| quote_literal(tablename) || ' and column_name= '|| quote_literal(colname)
into col_name ;
raise info ' the val : % ', col_name;
if(col_name is null ) then
col_name := colname;
execute 'alter table ' ||schemaname|| '.'|| tablename || ' add column '|| colname || ' ' || coltype;
else
col_name := colname ||' Already exist';
end if;
return col_name;
end;
$$
This is basically the solution from sola, but just cleaned up a bit. It's different enough that I didn't just want to "improve" his solution (plus, I sort of think that's rude).
Main difference is that it uses the EXECUTE format. Which I think is a bit cleaner, but I believe means that you must be on PostgresSQL 9.1 or newer.
This has been tested on 9.1 and works. Note: It will raise an error if the schema/table_name/or data_type are invalid. That could "fixed", but might be the correct behavior in many cases.
CREATE OR REPLACE FUNCTION add_column(schema_name TEXT, table_name TEXT,
column_name TEXT, data_type TEXT)
RETURNS BOOLEAN
AS
$BODY$
DECLARE
_tmp text;
BEGIN
EXECUTE format('SELECT COLUMN_NAME FROM information_schema.columns WHERE
table_schema=%L
AND table_name=%L
AND column_name=%L', schema_name, table_name, column_name)
INTO _tmp;
IF _tmp IS NOT NULL THEN
RAISE NOTICE 'Column % already exists in %.%', column_name, schema_name, table_name;
RETURN FALSE;
END IF;
EXECUTE format('ALTER TABLE %I.%I ADD COLUMN %I %s;', schema_name, table_name, column_name, data_type);
RAISE NOTICE 'Column % added to %.%', column_name, schema_name, table_name;
RETURN TRUE;
END;
$BODY$
LANGUAGE 'plpgsql';
usage:
select add_column('public', 'foo', 'bar', 'varchar(30)');
Can be added to migration scripts invoke function and drop when done.
create or replace function patch_column() returns void as
$$
begin
if exists (
select * from information_schema.columns
where table_name='my_table'
and column_name='missing_col'
)
then
raise notice 'missing_col already exists';
else
alter table my_table
add column missing_col varchar;
end if;
end;
$$ language plpgsql;
select patch_column();
drop function if exists patch_column();
In my case, for how it was created reason it is a bit difficult for our migration scripts to cut across different schemas.
To work around this we used an exception that just caught and ignored the error. This also had the nice side effect of being a lot easier to look at.
However, be wary that the other solutions have their own advantages that probably outweigh this solution:
DO $$
BEGIN
BEGIN
ALTER TABLE IF EXISTS bobby_tables RENAME COLUMN "dckx" TO "xkcd";
EXCEPTION
WHEN undefined_column THEN RAISE NOTICE 'Column was already renamed';
END;
END $$;
You can do it by following way.
ALTER TABLE tableName drop column if exists columnName;
ALTER TABLE tableName ADD COLUMN columnName character varying(8);
So it will drop the column if it is already exists. And then add the column to particular table.
Simply check if the query returned a column_name.
If not, execute something like this:
ALTER TABLE x ADD COLUMN y int;
Where you put something useful for 'x' and 'y' and of course a suitable datatype where I used int.