sqitchers / sqitch

Sensible database change management

Home Page:https://sqitch.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

PL/pgSQL and variables in deployment scripts

gleme opened this issue · comments

Me and my team just started to use sqitch to manage our database project, and we are trying to use PL/pgSQL in the scripts with variables, but we can't make it work. The current script we are trying to run is actually something not so common, because we use the FORMAT function to generate a PL/pgSQL command and run it using \gexec. Like the example below:

SELECT FORMAT($$
        DO
        $do$
        DECLARE
            _user VARCHAR(63) := %s;
        BEGIN
            ASSERT (SELECT has_schema_privilege(_user, 'my_schema', 'usage'));
        END
        $do$;
    $$, :'user') \gexec;

In the example above we are trying to verify if a created role (user) has usage permissions in my_schema. The role was created using a variable as well, but using regular PG SQL statements. The next thing we would like to try is something like:

DO
$do$
BEGIN
   IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE  rolname = :'user') THEN
       CREATE USER :"user" WITH ENCRYPTED PASSWORD :'password';
       GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES ON ALL TABLES IN SCHEMA my_schema TO :"user";
       GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA my_schema TO :"user";
       GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA my_schema TO :"user";
       GRANT USAGE ON SCHEMA my_schema TO :"user";
       ALTER DEFAULT PRIVILEGES IN SCHEMA my_schema GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES ON TABLES TO :"user";
   END IF;
END
$do$;

The major goal is to create idempotent scripts, which the CREATE USER statement doesn't support, that's why we would like to run PL/pgSQL instead, but in this case we also would like to use variables, so for example an user and password is not hardcoded in our scripts codebase.

I'm sorry if I missed something, but I couldn't find anything in the docs related to this kind of usage.

Thanks in advance! 😄

Here's how I have only created a user that did not already exist:

-- Deploy roles 

BEGIN; 
SET search_path TO public; 

DO $$ 
BEGIN 
    PERFORM true FROM pg_roles WHERE rolname = 'editor'; 
    IF NOT FOUND THEN 
        CREATE ROLE editor WITH NOLOGIN; 
    END IF; 
END; 
$$; 

COMMIT; 

The revert script looks like this:

-- Revert roles 
-- Do nothing; leave the roles in place. 

Yep, just comments.

Note I tend to create roles with NOLOGIN then GRANT permissions in the deploy scripts that create objects. Then when we deploy, we create a role with a login and add it to the NOLOGIN role:

psql -d stories -c "CREATE USER fred PASSWORD '****' IN ROLE editor” 

Using variables is harder, because psql :varname variables are evaluated by the psql CLI, not SQL, and you can't use format() or string concatenation with DO. What you can do is create a temporary function that takes arguments and then call it, something like this (untested, might requires some fiddling:

CREATE OR REPLACE TEMPORARY FUNCTION coruser(
    username text
    password text
) language plpsql as $f$
BEGIN
   IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = username) THEN
      EXECUTE format($$
       CREATE USER %1I WITH ENCRYPTED PASSWORD %2L;
       GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES ON ALL TABLES IN SCHEMA my_schema TO %1I;
       GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA my_schema TO %1I;
       GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA my_schema TO %1I;
       GRANT USAGE ON SCHEMA my_schema TO %1I;
       ALTER DEFAULT PRIVILEGES IN SCHEMA my_schema GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES ON TABLES TO %1I;
    $$, username, password);
   END IF;
END
$f$;

SELECT coruser(:'user', :'password');

HTH!