I'm using PostgreSQL and am an SQL beginner. I'm trying to create a table from a query, and if I run:
CREATE TABLE table_name AS
(....query...)
it works just fine. But then if I add 'if not exists' and run:
CREATE TABLE IF NOT EXISTS table_name AS
(....query...)
using exactly the same query, I get:
ERROR: syntax error at or near "as"
Is there any way to do this?
CREATE TABLE AS is considered a separate statement from a normal CREATE TABLE, and until Postgres version 9.5 (see changelog entry) didn't support an IF NOT EXISTS clause. (Be sure to look at the correct version of the manual for the version you are using.)
Although not quite as flexible, the CREATE TABLE ... LIKE syntax might be an alternative in some situations; rather than taking its structure (and content) from a SELECT statement, it copies the structure of another table or view.
Consequently, you could write something like this (untested); the final insert is a rather messy way of doing nothing if the table is already populated:
CREATE OR REPLACE VIEW source_data AS SELECT * FROM foo NATURAL JOIN bar;
CREATE TABLE IF NOT EXISTS snapshot LIKE source_data;
INSERT INTO snapshot
SELECT * FROM source_data
WHERE NOT EXISTS ( SELECT * FROM snapshot );
Alternatively, if you want to discard previous data (e.g. an abandoned temporary table), you could conditionally drop the old table, and unconditionally create the new one:
DROP TABLE IF EXISTS temp_stuff;
CREATE TEMPORARY TABLE temp_stuff AS SELECT * FROM foo NATURAL JOIN bar;
CREATE TABLE IF NOT EXISTS ... was added in Postgres 9.1. See:
PostgreSQL create table if not exists
Postgres 9.0 or older
If you are going to write a function for this, base it on system catalog table pg_class, not on views in the information schema or the statistics collector (which only exist if activated).
How to check if a table exists in a given schema
CREATE OR REPLACE FUNCTION create_table_qry(_tbl text
, _qry text
, _schema text = NULL)
RETURNS bool
LANGUAGE plpgsql AS
$func$
DECLARE
_sch text := COALESCE(_schema, current_schema());
BEGIN
IF EXISTS (
SELECT FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = _sch
AND c.relname = _tbl
) THEN
RAISE NOTICE 'Name is not free: %.%',_sch, _tbl;
RETURN FALSE;
ELSE
EXECUTE format('CREATE TABLE %I.%I AS %s', _sch, _tbl, _qry);
RAISE NOTICE 'Table created successfully: %.%',_sch, _tbl;
RETURN TRUE;
END IF;
END
$func$;
The function takes a table name and the query string, and optionally also a schema to create the table in (defaults to the current schema).
Note the correct use of = in the function header and := in the function body:
The forgotten assignment operator "=" and the commonplace ":="
Also note how identifiers are escaped as identifiers. You can't use regclass, since the table does not exist, yet:
Table name as a PostgreSQL function parameter
Try this,
create or replace function create_table(tblname text) returns text as
$$
BEGIN
$1 = trim($1);
IF not EXISTS (select relname from pg_stat_user_tables where relname =$1) THEN
execute 'create table '||$1||' as select * from tbl'; -- <put your query here>
return ''||$1||' Created Successfully !!';
else
return ''||$1||' Already Exists !!';
END IF;
END
$$
language plpgsql
create or replace function create_table_qry(tblname text,qry text) returns text as
$$
BEGIN
$1 = trim($1);
IF not EXISTS (select relname from pg_stat_user_tables where relname =$1) THEN
execute 'create table '||$1||' as '||$2||'';
return ''||$1||' Created Successfully !!';
else
return ''||$1||' Already Exists !!';
END IF;
END
$$
language plpgsql
It’s simple:
CREATE TABLE IF NOT EXISTS abc ( sql_id BIGINT(20) NOT NULL
AUTO_INCREMENT PRIMARY KEY, sender VARCHAR(20) NULL)
Use do :
do $$ begin
if not exists ( SELECT 1
FROM information_schema.tables
WHERE table_schema = 'schema_name'
AND table_name = 'bla ') then
create table schema_name.bla as select * from blu;
end if;
end $$;
CTAS (Create Table AS) for REDSHIFT PLPGSQL flavor. Shout out to Erwin Brandstetter for the root idea using pure PG syntax.
CREATE
OR
REPLACE
PROCEDURE pipeline.sp_create_table_if_not_exists_as (sch VARCHAR, tbl VARCHAR, qry VARCHAR, tbl_attrs VARCHAR)
AS
/*
specifically an exception for CTAS functionality: https://docs.aws.amazon.com/redshift/latest/dg/r_CREATE_TABLE_AS.html
*/
$$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = sch
AND c.relname = tbl
) THEN
RAISE INFO 'Table already exists: %.%', sch, tbl;
ELSE
EXECUTE 'CREATE TABLE ' || sch || '.' || tbl || ' ' || tbl_attrs || ' AS ' || qry;
RAISE INFO 'Table created successfully: %.%, using query: [%], optional attributes: [%]', sch, tbl, qry, tbl_attrs;
END IF;
END;
$$
language plpgsql;
Related
I would like to enforce a rule such that when people are creating table without primary key, it throws an error. Is it possible to be done from within pgdb?
DROP EVENT TRIGGER trig_test_event_trigger_table_have_primary_key;
CREATE OR REPLACE FUNCTION test_event_trigger_table_have_primary_key ()
RETURNS event_trigger
LANGUAGE plpgsql
AS $$
DECLARE
obj record;
object_types text[];
table_name text;
BEGIN
FOR obj IN
SELECT
*
FROM
pg_event_trigger_ddl_commands ()
LOOP
RAISE NOTICE 'classid: % objid: %,object_type: %
object_identity: % schema_name: % command_tag: %' , obj.classid , obj.objid , obj.object_type , obj.object_identity , obj.schema_name , obj.command_tag;
IF obj.object_type ~ 'table' THEN
table_name := obj.object_identity;
END IF;
object_types := object_types || obj.object_type;
END LOOP;
RAISE NOTICE 'table name: %' , table_name;
IF EXISTS (
SELECT
FROM
pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid
AND a.attnum = ANY (i.indkey)
WHERE
i.indisprimary
AND i.indrelid = table_name::regclass) IS FALSE THEN
RAISE EXCEPTION ' no primary key, this table not created';
END IF;
END;
$$;
CREATE EVENT TRIGGER trig_test_event_trigger_table_have_primary_key ON ddl_command_end
WHEN TAG IN ('CREATE TABLE')
EXECUTE FUNCTION test_event_trigger_table_have_primary_key ();
demo:
DROP TABLE a3;
DROP TABLE a4;
DROP TABLE a5;
CREATE TABLE a3 (
a int
);
CREATE TABLE a4 (
a int PRIMARY KEY
);
CREATE TABLE a5 (
a1 int UNIQUE
);
Only table a4 will be created.
related post: PL/pgSQL checking if a row exists
https://wiki.postgresql.org/wiki/Retrieve_primary_key_columns
EDIT: Someone else has answered regarding how to test the existence of primary keys, which completes Part 2 below. You will have to combine both answers for the full solution.
The logic fits inside several event triggers (also see documentation for the create command).
First point to note is the DDL commands this can apply to, all documented here.
Part 1: CREATE TABLE AS & SELECT INTO
If I am not wrong, CREATE TABLE AS and SELECT INTO never add constraints on the created table, they must be blocked with an event trigger that always raises an exception.
CREATE OR REPLACE FUNCTION block_ddl()
RETURNS event_trigger
LANGUAGE plpgsql AS
$$
BEGIN
RAISE EXCEPTION 'It is forbidden to create tables using command: %', tg_tag ;
END;
$$;
CREATE EVENT TRIGGER AdHocTables_forbidden
ON ddl_command_end
WHEN TAG IN ('CREATE TABLE AS', 'SELECT INTO')
EXECUTE FUNCTION block_ddl();
Note your could define the trigger to be ON ddl_command_start`. It makes it a little bit faster but does not go well with the full code I posted at the end.
See the next, less straightforward part for the rest of the explanations.
Part 2: Regular CREATE TABLE & ALTER TABLE
This case is more complex, as we want to block only some commands but not all.
The function and event trigger below do:
Output the whole command being passed.
Break the command into its subparts.
To do it, it uses the pg_event_trigger_ddl_commands() (documentation here), which BTW is the reason why this trigger had to be on ddl_command_end.
You will note that when adding a primary key, a CREATE INDEX is caught too.
In the case of the function below, raises an exception to block the creation in all cases (so you can test it without dropping the table you create every time).
Here is the code:
CREATE OR REPLACE FUNCTION pk_enforced()
RETURNS event_trigger
LANGUAGE plpgsql AS
$$
DECLARE r RECORD;
BEGIN
RAISE NOTICE 'Caught command %', (SELECT current_query());
FOR r IN SELECT * FROM pg_event_trigger_ddl_commands() LOOP
RAISE NOTICE 'Caught inside command % (%)', r.command_tag, r.object_identity;
END LOOP;
RAISE EXCEPTION 'Blocking the Creation';
END;
$$;
CREATE EVENT TRIGGER pk_is_mandatory
ON ddl_command_end
WHEN TAG IN ('CREATE TABLE', 'ALTER TABLE')
EXECUTE FUNCTION pk_enforced();
Additional notes:
You can prevent these constraints from being enforced on a temporary table by tested the schema_name is not pg_temp. The full code, including this test and with credit to jian for the function he posted:
CREATE OR REPLACE FUNCTION public.pk_enforced()
RETURNS event_trigger
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
obj RECORD;
table_name text;
BEGIN
FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands ()
LOOP
IF obj.schema_name = 'pg_temp' THEN
return;
END IF;
IF obj.object_type ~ 'table' THEN
table_name := obj.object_identity;
END IF;
END LOOP;
IF NOT EXISTS (
SELECT
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY (i.indkey)
WHERE i.indrelid = table_name::regclass
AND (i.indisprimary OR i.indisunique)) THEN
RAISE EXCEPTION 'A primary key or a unique constraint is mandatory to perform % on %.', tg_tag, obj.object_identity;
END IF;
END;
$BODY$;
CREATE OR REPLACE FUNCTION public.block_ddl()
RETURNS event_trigger
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
obj RECORD;
BEGIN
FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands ()
LOOP
IF obj.schema_name = 'pg_temp' THEN
return;
END IF;
END LOOP;
RAISE EXCEPTION 'DDL command ''%'' is blocked.', tg_tag ;
END;
$BODY$;
CREATE EVENT TRIGGER pk_is_mandatory ON DDL_COMMAND_END
WHEN TAG IN ('CREATE TABLE', 'ALTER TABLE')
EXECUTE PROCEDURE public.pk_enforced();
CREATE EVENT TRIGGER adhoctables_forbidden ON DDL_COMMAND_END
WHEN TAG IN ('CREATE TABLE AS', 'SELECT INTO')
EXECUTE PROCEDURE public.block_ddl();
i'm creating a trigger that triggers on INSERT on a table,
and i wish to log the structure of tables inserted so i wrote this Function
CREATE OR REPLACE FUNCTION update_table_log_received()
RETURNS TRIGGER AS $$
DECLARE
added_column TEXT;
target_table_name TEXT;
old_column text;
BEGIN
-- Check if a new column has been added
IF (TG_OP = 'INSERT') THEN
added_column := NEW."COLUMN_NAME";
target_table_name := NEW."TABLE_NAME";
END IF;
SELECT column_name into old_column
FROM information_schema."columns"
WHERE table_schema = 'items'
and table_name = LOWER(NEW."TABLE_NAME")
and column_name = LOWER(NEW."COLUMN_NAME");
if (coalesce(old_column,'')='' or old_column='' or old_column = added_column) THEN
-- If a new column has been added
IF (Lower(added_column) != 'sync') then
-- Add the new column to the target table
EXECUTE 'ALTER TABLE items.' || LOWER(target_table_name)|| ' ADD COLUMN ' || LOWER(added_column) || ' VARCHAR(50)';
END IF;
end if;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
executed by this TRIGGER :
CREATE TRIGGER update_table_log_received_trigger
AFTER INSERT
ON items."TABLE_LOG_RECEIVED"
FOR EACH ROW
EXECUTE FUNCTION update_table_log_received();
the returned exception is the following :
! ERROR: the column « x » of the relation « y » already exists
Where: instruction SQL « ALTER TABLE items. ADD COLUMN x VARCHAR(50) »
my problem now is that it isn't supposed to pass the If checks (i pasted the code after many alterations i have two if conditions that do the same thing just because),
i debugged and logged the statements to note that the select query inside my function returns null apparently.
i also tried to use "USING NEW" but i am no expert so i couldn't make it work
is it a problem with the declared variable not being populated from the "NEW" record or am i executing the select statement wrong ?
EDIT : tl;dr for my problem, I would like to update a table in Database2 whenever the same table (that had the same structre) is altered from Database1, be it added column or changed column, at this point iI'm stuck at the first problem to add the column.
I am logging my tables' structures as strings into a new table and syncing that with Database2 to then have the trigger alter the same altered table from Database1, hope this makes more sense now.
Database1 log_table that logs all my tables' structures:
Database2 log_table_received that is a copy of log_table that executes
the trigger whenever new values are inserted;
Try this syntax:
CREATE OR REPLACE FUNCTION update_table_log_received()
RETURNS TRIGGER AS $$
DECLARE
added_column TEXT;
target_table_name TEXT;
old_column text;
BEGIN
-- Check if a new column has been added
IF (TG_OP = 'INSERT') THEN
added_column := new."column_name";
target_table_name := new."table_name";
END IF;
if not exists(select 1 from information_schema."columns" where table_name = target_table_name and column_name = added_column)
then
EXECUTE 'ALTER TABLE items.' || LOWER(target_table_name)|| ' ADD COLUMN ' || LOWER(added_column) || ' VARCHAR(50)';
end if;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
I tried on my DB this is works. You can change some details yourself.
Fixed; Question should have been :
How to select tables & table columns inside function in postgresql.
References:
How to add column if not exists on PostgreSQL?
How to check if a table exists in a given schema
How to get a list column names and datatypes of a table in PostgreSQL?
Basically information_schema can only be accessed by owner meaning the user or (i) see the result when i query it but it returns FALSE when executed inside a script more details here :
https://stackoverflow.com/a/24089729/15170264
Full trigger after fix with CTE to query the pg_catalog also added ADD COLUMN IF NOT EXISTS in my Execute query just to be safe
CREATE OR REPLACE FUNCTION update_table_log_received()
RETURNS TRIGGER AS $$
DECLARE
added_column TEXT;
target_table_name TEXT;
old_column varchar;
old_table varchar;
BEGIN
-- Check if a new column has been added
IF (TG_OP = 'INSERT') THEN
added_column := NEW."COLUMN_NAME";
target_table_name := NEW."TABLE_NAME";
END IF;
/*
* --------------- --CTE to find Columns of table "Target_table_name" from pg_catalog
*/
WITH cte_tables AS (
SELECT
pg_attribute.attname AS column_name,
pg_catalog.format_type(pg_attribute.atttypid, pg_attribute.atttypmod) AS data_type
FROM
pg_catalog.pg_attribute
INNER JOIN
pg_catalog.pg_class ON pg_class.oid = pg_attribute.attrelid
INNER JOIN
pg_catalog.pg_namespace ON pg_namespace.oid = pg_class.relnamespace
WHERE
pg_attribute.attnum > 0
AND NOT pg_attribute.attisdropped
AND pg_namespace.nspname = 'items'
AND pg_class.relname = 'trace'
ORDER BY
attnum ASC
)
select column_name into old_column from cte_tables where
column_name=LOWER(added_column);
if (old_column is null ) then
-- Add the new column to the target table
old_column := added_column;
EXECUTE 'ALTER TABLE items.' || LOWER(target_table_name)|| ' ADD COLUMN IF NOT EXISTS ' || LOWER(added_column) || ' VARCHAR(50)';
else
old_column := added_column || 'already exists ! ';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
create TRIGGER update_table_log_received_trigger
AFTER INSERT
ON items."TABLE_LOG_RECEIVED"
FOR EACH ROW
EXECUTE FUNCTION update_table_log_received();
Variable old_column stores the else condition message but i do not return it, would have if it was a simple function.
I have the following code to create a function that truncates all rows from the table web_channel2 if the table is not empty:
create or replace function truncate_if_exists(tablename text)
returns void language plpgsql as $$
begin
select
from information_schema.tables
where table_name = tablename;
if found then
execute format('truncate %I', tablename);
end if;
end $$;
Unfortunately I don't know how should I continue ...
How to execute the function?
TLDR
To execute a Postgres function (returning void), call it with SELECT:
SELECT truncate_if_exists('web_channel2');
Proper solution
... how should I continue?
Delete the function again.
DROP FUNCTION truncate_if_exists(text);
It does not offer any way to schema-qualify the table. Using it might truncate the wrong table ...
Looks like you are trying to avoid an exception if the table is not there.
And you only want to truncate ...
if the table is not empty
To that end, I might use a safe function like this:
CREATE OR REPLACE FUNCTION public.truncate_if_exists(_table text, _schema text DEFAULT NULL)
RETURNS text
LANGUAGE plpgsql AS
$func$
DECLARE
_qual_tbl text := concat_ws('.', quote_ident(_schema), quote_ident(_table));
_row_found bool;
BEGIN
IF to_regclass(_qual_tbl) IS NOT NULL THEN -- table exists
EXECUTE 'SELECT EXISTS (SELECT FROM ' || _qual_tbl || ')'
INTO _row_found;
IF _row_found THEN -- table is not empty
EXECUTE 'TRUNCATE ' || _qual_tbl;
RETURN 'Table truncated: ' || _qual_tbl;
ELSE -- optional!
RETURN 'Table exists but is empty: ' || _qual_tbl;
END IF;
ELSE -- optional!
RETURN 'Table not found: ' || _qual_tbl;
END IF;
END
$func$;
To execute, call it with SELECT:
SELECT truncate_if_exists('web_channel2');
If no schema is provided, the function falls back to traversing the search_path - like your original did. If that's unreliable, or generally, to be safe (which seems prudent when truncating tables!) provide the schema explicitly:
SELECT truncate_if_exists('web_channel2', 'my_schema');
db<>fiddle here
When providing identifiers as strings, you need to use exact capitalization.
Why the custom variable _row_found instead of FOUND? See:
Dynamic SQL (EXECUTE) as condition for IF statement
Basics:
Table name as a PostgreSQL function parameter
How to check if a table exists in a given schema
PL/pgSQL checking if a row exists
How does the search_path influence identifier resolution and the "current schema"
Are PostgreSQL column names case-sensitive?
I'm trying to create a PostgreSQL stored procedure that must set default value 'now()' to each columns named 'CreationDate' in every tables.
CREATE OR REPLACE FUNCTION set_creation_date() RETURNS
void AS $$
DECLARE
t pg_tables%ROWTYPE;
BEGIN
FOR t IN SELECT "tablename" FROM pg_tables WHERE "schemaname" = 'public' LOOP
IF EXISTS (select * from information_schema.columns where table_name = t."tablename"
and column_name = 'CreationDate') THEN
EXECUTE FORMAT('ALTER TABLE %I ALTER COLUMN "CreationDate" SET DEFAULT now()', t."tablename");
END IF;
END LOOP;
END;
$$ LANGUAGE plpgsql;
But no columns are affected. What's wrong?
It looks like it's trying to alter t."tablename" every time. Try the following instead:
EXECUTE FORMAT('ALTER TABLE %I ALTER COLUMN "CreationDate" SET DEFAULT now()', t."tablename");
I don't have a PostgreSQL server available to test, so please let me know if the syntax isn't quite right.
I finally resolved. The error was in pg_tables%ROWTYPE: using debugger i realized that the field 'tablename' doesn't exist, so i declared 't' as text.
I created a function with parameters, where you have to pass schema name and column name. I ll post it here for anyone who ll need it
DROP FUNCTION IF EXISTS set_creation_date(character varying, character varying);
CREATE OR REPLACE FUNCTION set_creation_date(_schema_name character varying, _column_date character varying)
RETURNS void AS
$BODY$
DECLARE
t TEXT;
BEGIN
FOR t IN SELECT "tablename" FROM pg_tables WHERE "schemaname" = _schema_name LOOP
IF EXISTS (select * from information_schema.columns where table_name = t and column_name = _column_date) THEN
EXECUTE FORMAT('ALTER TABLE %I ALTER COLUMN %I SET DEFAULT now()', t, _column_date);
END IF;
END LOOP;
END;
$BODY$
LANGUAGE plpgsql;
I have a database with 169 tables
I need this column in every table:
wid integer not null primary key
I tried this(Thanks https://stackoverflow.com/users/27535/gbn for the solution):
SELECT
'ALTER TABLE ' + T.name + ' ADD foo int NULL'
FROM
sys.tables AS T
WHERE
T.is_ms_shipped = 0
But it didn't work on PostgreSQL.
It only worked on tsql.
How to add this column in every table at once ?
do $$
declare
selectrow record;
begin
for selectrow in
select
'ALTER TABLE '|| T.mytable || ' ADD COLUMN foo integer NULL' as script
from
(
select tablename as mytable from pg_tables where schemaname ='public' --your schema name here
) t
loop
execute selectrow.script;
end loop;
end;
$$;
You can test whether all your tables altered with the new column using the following select
select
table_name,COLUMN_NAME
from
INFORMATION_SCHEMA.COLUMNS
where
COLUMN_NAME='foo' -- column name here
Try this (change 'public' to whatever schema you're doing this in)
DO $$
DECLARE
row record;
cmd text;
BEGIN
FOR row IN SELECT schemaname, tablename FROM pg_tables WHERE schemaname = 'public' LOOP
cmd := format('ALTER TABLE %I.%I ADD COLUMN foo SERIAL PRIMARY KEY ', row.schemaname, row.tablename);
RAISE NOTICE '%', cmd;
-- EXECUTE cmd;
END LOOP;
END
$$ LANGUAGE plpgsql;
If you run as is, it'll show you the commands. Uncomment the EXECUTE line to actually perform the alterations.
I'd run within a transaction so you can roll back if you're not happy with the results.
Note that the type is SERIAL - the column type will be integer, but also creates a sequence owned by the table and defaults the column value to the next value of that sequence.
We may need to check column is already exist or not.
Tested on PostgreSQL V10
do $$
declare selectrow record;
begin
for selectrow in
select 'ALTER TABLE '|| T.mytable || ' ADD COLUMN x_is_exported boolean DEFAULT FALSE' as script
from (select tablename as mytable from pg_tables where schemaname ='public') t
loop
begin
execute selectrow.script;
EXCEPTION WHEN duplicate_column THEN CONTINUE;
END;
end loop;
end;
$$;