How do I write an SQL script to create a ROLE in PostgreSQL 9.1, but without raising an error if it already exists?
The current script simply has:
CREATE ROLE my_user LOGIN PASSWORD 'my_password';
This fails if the user already exists. I'd like something like:
IF NOT EXISTS (SELECT * FROM pg_user WHERE username = 'my_user')
BEGIN
CREATE ROLE my_user LOGIN PASSWORD 'my_password';
END;
... but that doesn't work - IF doesn't seem to be supported in plain SQL.
I have a batch file that creates a PostgreSQL 9.1 database, role and a few other things. It calls psql.exe, passing in the name of an SQL script to run. So far all these scripts are plain SQL and I'd like to avoid PL/pgSQL and such, if possible.
Simple script (question asked)
Building on #a_horse_with_no_name's answer and improved with #Gregory's comment:
DO
$do$
BEGIN
IF EXISTS (
SELECT FROM pg_catalog.pg_roles
WHERE rolname = 'my_user') THEN
RAISE NOTICE 'Role "my_user" already exists. Skipping.';
ELSE
CREATE ROLE my_user LOGIN PASSWORD 'my_password';
END IF;
END
$do$;
Unlike, for instance, with CREATE TABLE there is no IF NOT EXISTS clause for CREATE ROLE (up to at least Postgres 14). And you cannot execute dynamic DDL statements in plain SQL.
Your request to "avoid PL/pgSQL" is impossible except by using another PL. The DO statement uses PL/pgSQL as default procedural language:
DO [ LANGUAGE lang_name ] code
...
lang_name
The name of the procedural language the code is written in. If
omitted, the default is plpgsql.
No race condition
The above simple solution allows for a race condition in the tiny time frame between looking up the role and creating it. If a concurrent transaction creates the role in between we get an exception after all. In most workloads, that will never happen as creating roles is a rare operation carried out by an admin. But there are highly contentious workloads like #blubb mentioned.
#Pali added a solution trapping the exception. But a code block with an EXCEPTION clause is expensive. The manual:
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.
Actually raising an exception (and then trapping it) is comparatively expensive on top of it. All of this only matters for workloads that execute it a lot - which happens to be the primary target audience. To optimize:
DO
$do$
BEGIN
IF EXISTS (
SELECT FROM pg_catalog.pg_roles
WHERE rolname = 'my_user') THEN
RAISE NOTICE 'Role "my_user" already exists. Skipping.';
ELSE
BEGIN -- nested block
CREATE ROLE my_user LOGIN PASSWORD 'my_password';
EXCEPTION
WHEN duplicate_object THEN
RAISE NOTICE 'Role "my_user" was just created by a concurrent transaction. Skipping.';
END;
END IF;
END
$do$;
Much cheaper:
If the role already exists, we never enter the expensive code block.
If we enter the expensive code block, the role only ever exists if the unlikely race condition hits. So we hardly ever actually raise an exception (and catch it).
Some answers suggested to use pattern: check if role does not exist and if not then issue CREATE ROLE command. This has one disadvantage: race condition. If somebody else creates a new role between check and issuing CREATE ROLE command then CREATE ROLE obviously fails with fatal error.
To solve above problem, more other answers already mentioned usage of PL/pgSQL, issuing CREATE ROLE unconditionally and then catching exceptions from that call. There is just one problem with these solutions. They silently drop any errors, including those which are not generated by fact that role already exists. CREATE ROLE can throw also other errors and simulation IF NOT EXISTS should silence only error when role already exists.
CREATE ROLE throw duplicate_object error when role already exists. And exception handler should catch only this one error. As other answers mentioned it is a good idea to convert fatal error to simple notice. Other PostgreSQL IF NOT EXISTS commands adds , skipping into their message, so for consistency I'm adding it here too.
Here is full SQL code for simulation of CREATE ROLE IF NOT EXISTS with correct exception and sqlstate propagation:
DO $$
BEGIN
CREATE ROLE test;
EXCEPTION WHEN duplicate_object THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
END
$$;
Test output (called two times via DO and then directly):
$ sudo -u postgres psql
psql (9.6.12)
Type "help" for help.
postgres=# \set ON_ERROR_STOP on
postgres=# \set VERBOSITY verbose
postgres=#
postgres=# DO $$
postgres$# BEGIN
postgres$# CREATE ROLE test;
postgres$# EXCEPTION WHEN duplicate_object THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
postgres$# END
postgres$# $$;
DO
postgres=#
postgres=# DO $$
postgres$# BEGIN
postgres$# CREATE ROLE test;
postgres$# EXCEPTION WHEN duplicate_object THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
postgres$# END
postgres$# $$;
NOTICE: 42710: role "test" already exists, skipping
LOCATION: exec_stmt_raise, pl_exec.c:3165
DO
postgres=#
postgres=# CREATE ROLE test;
ERROR: 42710: role "test" already exists
LOCATION: CreateRole, user.c:337
Or if the role is not the owner of any db objects one can use:
DROP ROLE IF EXISTS my_user;
CREATE ROLE my_user LOGIN PASSWORD 'my_password';
But only if dropping this user will not make any harm.
Bash alternative (for Bash scripting):
psql -h localhost -U postgres -tc \
"SELECT 1 FROM pg_user WHERE usename = 'my_user'" \
| grep -q 1 \
|| psql -h localhost -U postgres \
-c "CREATE ROLE my_user LOGIN PASSWORD 'my_password';"
(isn't the answer for the question! it is only for those who may be useful)
Here is a generic solution using plpgsql:
CREATE OR REPLACE FUNCTION create_role_if_not_exists(rolename NAME) RETURNS TEXT AS
$$
BEGIN
IF NOT EXISTS (SELECT * FROM pg_roles WHERE rolname = rolename) THEN
EXECUTE format('CREATE ROLE %I', rolename);
RETURN 'CREATE ROLE';
ELSE
RETURN format('ROLE ''%I'' ALREADY EXISTS', rolename);
END IF;
END;
$$
LANGUAGE plpgsql;
Usage:
posgres=# SELECT create_role_if_not_exists('ri');
create_role_if_not_exists
---------------------------
CREATE ROLE
(1 row)
posgres=# SELECT create_role_if_not_exists('ri');
create_role_if_not_exists
---------------------------
ROLE 'ri' ALREADY EXISTS
(1 row)
The same solution as for Simulate CREATE DATABASE IF NOT EXISTS for PostgreSQL? should work - send a CREATE USER … to \gexec.
Workaround from within psql
SELECT 'CREATE USER my_user'
WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'my_user')\gexec
Workaround from the shell
echo "SELECT 'CREATE USER my_user' WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'my_user')\gexec" | psql
See accepted answer there for more details.
Building off of the other answers here, I wanted the ability to execute psql once against a .sql file to have it perform a set of initialization operations. I also wanted the ability to inject the password at the time of execution to support CI/CD scenarios.
-- init.sql
CREATE OR REPLACE FUNCTION pg_temp.create_myuser(theUsername text, thePassword text)
RETURNS void AS
$BODY$
DECLARE
duplicate_object_message text;
BEGIN
BEGIN
EXECUTE format(
'CREATE USER %I WITH PASSWORD %L',
theUsername,
thePassword
);
EXCEPTION WHEN duplicate_object THEN
GET STACKED DIAGNOSTICS duplicate_object_message = MESSAGE_TEXT;
RAISE NOTICE '%, skipping', duplicate_object_message;
END;
END;
$BODY$
LANGUAGE 'plpgsql';
SELECT pg_temp.create_myuser(:'vUsername', :'vPassword');
Invoking with psql:
NEW_USERNAME="my_new_user"
NEW_PASSWORD="password with 'special' characters"
psql --no-psqlrc --single-transaction --pset=pager=off \
--tuples-only \
--set=ON_ERROR_STOP=1 \
--set=vUsername="$NEW_USERNAME" \
--set=vPassword="$NEW_PASSWORD" \
-f init.sql
This will allow init.sql to be run either locally or by the CI/CD pipeline.
Notes:
I did not find a way to reference a file variable (:vPassword) directly in a DO anonymous function, hence the full FUNCTION to pass the arg. (see #Clodoaldo Neto's answer)
#Erwin Brandstetter's answer explains why we must use an EXECUTE and cannot use CREATE USER directly.
#Pali's answer explains the need for the EXCEPTION to prevent race conditions (which is why the \gexec approach is not recommended).
The function must be invoked in a SELECT statement. Use the -t/--tuples-only attribute in the psql command to clean up log output, as pointed out in #villy393's answer.
The function is created in a temporary schema, so it will be deleted automatically.
Quoting is handled properly, so no special character in password can cause errors or worse, security vulnerability.
My team was hitting a situation with multiple databases on one server, depending on which database you connected to, the ROLE in question was not returned by SELECT * FROM pg_catalog.pg_user, as proposed by #erwin-brandstetter and #a_horse_with_no_name. The conditional block executed, and we hit role "my_user" already exists.
Unfortunately we aren't sure of exact conditions, but this solution works around the problem:
DO
$body$
BEGIN
CREATE ROLE my_user LOGIN PASSWORD 'my_password';
EXCEPTION WHEN others THEN
RAISE NOTICE 'my_user role exists, not re-creating';
END
$body$
It could probably be made more specific to rule out other exceptions.
As you are on 9.x, you can wrap that into a DO statement:
do
$body$
declare
num_users integer;
begin
SELECT count(*)
into num_users
FROM pg_user
WHERE usename = 'my_user';
IF num_users = 0 THEN
CREATE ROLE my_user LOGIN PASSWORD 'my_password';
END IF;
end
$body$
;
If you have access to a shell, you can do this.
psql -tc "SELECT 1 FROM pg_user WHERE usename = 'some_use'" | grep -q 1 || psql -c "CREATE USER some_user"
For those of you who would like an explanation:
-c = run command in database session, command is given in string
-t = skip header and footer
-q = silent mode for grep
|| = logical OR, if grep fails to find match run the subsequent command
You can do it in your batch file by parsing the output of:
SELECT * FROM pg_user WHERE usename = 'my_user'
and then running psql.exe once again if the role does not exist.
I needed this in a Makefile to not fail the job when the user exists:
initdb:
psql postgres -c "CREATE USER foo CREATEDB PASSWORD 'bar'" || true
...
Related
Through Jenkins pipeline, if I get error ORA-01940: cannot drop a user that is currently connected, I want to perform some action.
FOr testing I should get that message. If I create user and log in with it to sql developer and try to drop it through pipeline it results into above error -
sh '''
sqlplus -s /nolog <<-EOF
connect system/system#orcl
DROP USER TEST_USER;
exit
EOF
'''
Is there any way I can connect using that user and drop it through pipeline so results into above error?
If you are running that part through a PIPELINE in Jenkins using shell script, there are some options:
Get a specific Return Code of the Operation
In this case, you might do the following
sh '''
sqlplus -s /nolog <<-EOF
connect system/system#orcl
whenever sqlerror exit 99
DROP USER TEST_USER;
exit
EOF
'''
This operation will return 99 in case you got the error. You must evaluate the the return of the operation using $?
Example
Session 1 ( as system )
SQL> create user test identified by Oracle_1 ;
User created.
SQL> grant connect to test ;
Grant succeeded.
I connect with the user test, and then with my system user I will try to drop it
SQL> whenever sqlerror exit 99
SQL> drop user test ;
drop user test
*
ERROR at line 1:
ORA-01940: cannot drop a user that is currently connected
Disconnected from Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 - 64 bit Production
[ftpcpl#scglvdoracp0040 ~]$ echo $?
99
Using PLSQL Block to return code or message
However, if you want something different than the option before, like having the code displayed automatically without evaluation, then you can get the message display by the output using an anonymous PL/SQL block.
In your case, I would use something like this:
sh '''
sqlplus -s /nolog <<-EOF
connect system/system#orcl
set serveroutput on feedback off
declare
user_is_conn exception;
verror varchar2(100);
pragma exception_init ( user_is_conn , -1940 );
begin
execute immediate q'[ DROP USER TEST ]';
exception
when user_is_conn then
verror := sqlerrm ; -- the whole error message
-- verror := sqlcode ; -- only the code 1940
dbms_output.put_line(verror); -- message
when others then
raise;
end;
/
exit
EOF
'''
I commented verror := sqlcode in the section before to show the whole message. An example of this running in Linux would be:
$ $ORACLE_HOME/bin/sqlplus -S "/ as sysdba" << eof
set serveroutput on feedback off
declare
user_is_conn exception;
verror varchar2(100);
pragma exception_init ( user_is_conn , -1940 );
begin
execute immediate 'DROP USER TEST' ;
exception
when user_is_conn then
verror := sqlerrm ;
dbms_output.put_line(verror);
when others then
raise;
end;
/
eof
ORA-01940: cannot drop a user that is currently connected
Be aware that this PL/SQL block is ended OK. I mean, from OS perspective the operation return code is 0. If you want that as a error, you need to raise the error, but in that case the message would be different.
I am failing to use a parameter in function declaration.
a SQL script like :
CREATE OR REPLACE FUNCTION test_functon() RETURNS trigger AS
$BODY$
DECLARE
test int:=:SRID;
BEGIN
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
in a file.
And running psql -v SRID=2056 -f my_file.sql
leads to the error
ERROR: syntax error at or near ":"
This looks like the SQL is run without the var being properly replaced by its value.
What is the solution for this?
It seems like psql does not interpolate variables in the body of functions.
The following SQL verifies that.
SELECT :SRID;
CREATE OR REPLACE FUNCTION test_functon() RETURNS :TYPE AS
$BODY$
BEGIN
RETURN :SRID;
END;
$BODY$
LANGUAGE plpgsql;
Running that file results in this:
$ psql -v SRID=2056 -v TYPE=int -f query.sql
Expanded display is used automatically.
Null display is "¤".
?column?
----------
2056
(1 row)
psql:query.sql:9: ERROR: syntax error at or near ":"
LINE 4: RETURN :SRID;
^
Notice how the return type defined by the variable TYPE is still interpolated, but everything inside the body is off limits.
You'll have to resort to a different mechanism to get your variable in there. You could leverage the fact that psql accepts a query through STDIN:
$ sed 's/:SRID/2056/' query.sql | psql
I have to merge path directory and actual time in psql command line.
This absolute path works well:
\copy (select row_to_json(row(column1,column5)) from testdatabase.test) TO 'C:\Users\path\file.json';
But when I want merge two string like this:
\copy (select row_to_json(row(column1,column5)) from testdatabase.test) TO 'C:\Users\path\' || to_char(now(), 'YYYY_MM_DD');
I get Permission denied. This is permission issue i think because first request works well on same path. I tried also CONCATE statement but it fails.
If using COPY directly is an option, you could dynamically build the command, including your custom path logic and EXECUTE it. For example in an anonymous DO block:
DO
$$
BEGIN
EXECUTE 'COPY (SELECT row_to_json(...) ...) TO ''C:\...\' || to_char(now(), 'YYYY_MM_DD') || '.json''';
END;
$$
LANGUAGE plpgsql;
I want to create a script that will have variables of _user and _pass to create the user in the Postgres database only if such login does not exist yet. I was thinking this would work, but i cant tell what is the issue:
DO
$DO$
DECLARE
_user TEXT := 'myuser';
_pass TEXT := 'user!pass';
BEGIN
IF NOT EXISTS ( SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = _user) THEN
RAISE NOTICE 'Creating user % ...',_user;
CREATE USER _user WITH
LOGIN
NOSUPERUSER
CREATEDB
CREATEROLE
NOREPLICATION
PASSWORD _pass;
RAISE NOTICE 'Created user %',_user;
ELSE
RAISE NOTICE 'User % already exists, not creating it',_user;
END IF;
END
$DO$
How do I enforce substitution of the variable with its content?
Also what is the difference between $DO$ and $$?
To parameterize identifiers or syntax elements, you generally need to use dynamic SQL with EXECUTE - best combined with format() for ease of use.
But utility commands (incl. all SQL DDL statements) do not allow passing of values or parameter substitution at all. You need to concatenate the complete statement before executing it. See:
“ERROR: there is no parameter $1” in “EXECUTE .. USING ..;” statement in plpgsql
Your code would work like this:
DO
$do$
DECLARE
_user text := 'myuser';
_pass text := 'user!pass';
BEGIN
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = _user) THEN
EXECUTE format(
'CREATE USER %I WITH
LOGIN
NOSUPERUSER
CREATEDB
CREATEROLE
NOREPLICATION
PASSWORD %L'
, _user
, _pass
);
RAISE NOTICE 'Created user "%"', _user;
ELSE
RAISE NOTICE 'User "%" already exists, not creating it', _user;
END IF;
END
$do$
But while _user and _pass are hardcoded anyway, you might simplify like demonstrated here:
Create PostgreSQL ROLE (user) if it doesn't exist
Also what is the difference between $DO$ and $$?
See:
What are '$$' used for in PL/pgSQL
Another way of ab(using) customized options ( quoting Erwin's older answers) :
src: https://stackoverflow.com/a/49175321/65706
src: https://dba.stackexchange.com/a/213921/1245
which could enable the passing of the variables from bash script as well :
aka in bash:
set -x
PGPASSWORD="${postgres_db_useradmin_pw:-}" psql -q -t -X \
-w -U "${postgres_db_useradmin:-}" \
-h $postgres_db_host -p $postgres_db_port \
-v ON_ERROR_STOP=1 \
-v postgres_db_user="${postgres_db_user:-}" \
-v postgres_db_user_pw="${postgres_db_user_pw:-}" \
-v postgres_db_name="${postgres_db_name:-}" \
-f "$sql_script" "${postgres_db_name:-}" > "$tmp_log_file" 2>&1
and in pgsql
SET myvars.postgres_db_user TO :'postgres_db_user';
SET myvars.postgres_db_user_pw TO :'postgres_db_user_pw';
DO
$do$
BEGIN
EXECUTE format(
'CREATE ROLE %I WITH PASSWORD %L LOGIN'
, current_setting('myvars.postgres_db_user', true)::text
, current_setting('myvars.postgres_db_user_pw', true)::text
);
RAISE NOTICE 'Created user "%"',
current_setting('myvars.postgres_db_user', true)::text;
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'User "%" already exists, not creating it',
current_setting('myvars.postgres_db_user', true)::text;
EXECUTE format(
'ALTER ROLE %I WITH PASSWORD %L LOGIN'
, current_setting('myvars.postgres_db_user', true)::text
, current_setting('myvars.postgres_db_user_pw', true)::text
);
END
$do$;
worth noting that often the same result might be achieved much more easier by means of a combination of bash vars interpolation and sql like this:
https://github.com/YordanGeorgiev/qto/blob/master/src/bash/qto/funcs/provision-db-admin.func.sh
So a while ago I was working with triggers and added a few to one of my tables for insert and update events and thus they went to my schema.rb but then I found a way not to use triggers and removed them from the dabatase console but their sql code obviously hadn't been deleted from the schema file, so now I have something like that in it:
execute(<<-TRIGGERSQL)
CREATE OR REPLACE FUNCTION public.arguments_vector_update()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
IF TG_OP = 'INSERT' THEN
new.tsv_body = to_tsvector('pg_catalog.simple', COALESCE(NEW.text, ''));
END IF;
IF TG_OP = 'UPDATE' THEN
IF NEW.text <> OLD.text THEN
new.tsv_body = to_tsvector('pg_catalog.simple', COALESCE(NEW.text, ''));
END IF;
END IF;
RETURN NEW;
END
$function$
TRIGGERSQL
and I think I've deleted the migration file too -_-
I could probably add a new migration with the code to remove the trigers but it'd make the schema file even messier, so I wonder if there is another way to remove this code from the schema file ?
I am using PSQL, if it's important.
rake -T in a Rails app will have this to say:
rake db:schema:dump # Create a db/schema.rb file that is portable against any DB supported by AR
So you can rake db:schema:dump to generate a fresh db/schema.rb file any time you need one.