1
votes

I'm playing around with plpgsql and have put together a function that assembles a dynamic query. I've tested it and it executes (i've included a test wrapper to output the assembled query).

Where I'm stumbling is capturing the output of the EXECUTE command once it's run, because I'd like to return some or all of the values, depending on the nature of the dynamic query. I've set up a type userprofile, and have the setProfileDynamic function return this type.

With the full complement of parameters, the output checks out (except for the second query, more on that in a bit). But when some of the parameters are missing (ie not all the user preferences are updated, only one eg: measuresystem) then the output is corrupted, so that measuresystem_id might appear as username in the output.

Secondly, is how to get the result of the second query (updateDefaultMealplan) into the userprofile type (where the columns mealplan_id and mealplan_name are waiting patiently). Currently this query returns into mp_id (and mp_name is filled from the _values array if 'defaultmealplan' key is present).

I'm quite new to this and I might be trying to do too much in one function, and I might be doing it completely the wrong way, so I don't mind whatever corrections might come to pass.

The userprofile type:

DROP TYPE IF EXISTS userprofile CASCADE;
CREATE TYPE userprofile AS (
    username text,
    measuresystem_id int,
    blanksymbol_id int,
    mealplan_id int,
    mealplan_name text
);

The main function

DROP FUNCTION IF EXISTS setProfileDynamic (int, text, text[], text[]);
CREATE OR REPLACE FUNCTION setProfileDynamic (_userid int, _token text, _keys text[], _values text[])
RETURNS userprofile AS $$
DECLARE
    _query text;
    numkeys int;
    i int;

    _update text[];
    _from text[];
    _where text[];
    _return text[];
    _into text[];
    test text[];

    up userprofile;
    mp_name text;
    mp_id int;

    u text;
    f text;
    w text;
    r text;

    c_update int := 1;
    c_from int := 1;
    c_where int := 3;
    c_return int := 1;

    runupdate boolean := false; --bc passing default mealplan through this fn too.
    changedefaultmp boolean := false;
 BEGIN
    test[1] := 'users.id';
    test[2] := 'users.token';
    test[3] := _userid;
    test[4] := _token;
    numkeys := array_length(_keys, 1);
    raise notice 'numkeys = %', numkeys;
    _where[1] := test[1] || ' = ' || quote_literal(test[3]);
    _where[2] :=  test[2] || ' = ' || quote_literal(test[4]);
    --raise notice '_where[1] = %', _where[1];
    --raise notice '_where[2] = %', _where[2];
    for i in 1..numkeys loop
        raise notice 'keys[%] = %', i, _keys[i];
        CASE _keys[i]
            WHEN 'email' THEN 
                runupdate := true;
                _update[c_update] := quote_ident(_keys[i]) || ' = ' || quote_literal(_values[i]);
                c_update := c_update + 1;
            WHEN 'password' THEN
                runupdate := true;
                _update[c_update] := quote_ident(_keys[i]) || ' = ' || quote_literal(_values[i]);
                c_update := c_update + 1;
            WHEN 'username' THEN
                runupdate := true;
                _update[c_update] := quote_ident(_keys[i]) || ' = ' || quote_literal(_values[i]);
                c_update := c_update + 1;
                _return[c_return] := quote_ident(_keys[i]);
                c_return := c_return + 1;
            WHEN 'measuresystem' THEN
                runupdate := true;
                _update[c_update] := 'measuresystem_id = ms.id';
                c_update := c_update + 1;
                _from[c_from] := 'measuresystem as ms';
                c_from := c_from + 1;
                _where[c_where] := 'ms.name = ' || quote_literal(_values[i]);
                c_where := c_where + 1;
                _return[c_return] := 'ms.id';
                c_return := c_return + 1;
            WHEN 'blanksymbol' THEN
                runupdate := true;
                _update[c_update] := 'blanksymbol_id = bs.id';
                c_update := c_update + 1;
                _from[c_from] := 'blanksymbol as bs';
                c_from := c_from + 1;
                _where[c_where] := 'bs.name = ' || quote_literal(_values[i]);
                c_where := c_where + 1;
                _return[c_return] := 'bs.id';
                c_return := c_return + 1;
            ELSE
                changedefaultmp := true;
                mp_name := _values[i];
        END CASE;
    end loop;
    u := 'UPDATE users SET ' || array_to_string(_update, ', ');
    f := 'FROM ' || array_to_string(_from, ', '); --if a_t_s is null, the whole f is null and not included so no error
    w := 'WHERE ' || array_to_string(_where, ' AND ');
    r := 'RETURNING ' || array_to_string(_return, ', ');

    raise notice 'u = %', u;
    raise notice 'f = %', f;
    raise notice 'w = %', w;
    raise notice 'r = %', r;

    _query = concat_ws(' ', u, f, w, r);
    raise notice '_query = %', _query;
    IF runupdate THEN
        if r IS NULL THEN
            EXECUTE _query;
        ELSE
            EXECUTE _query INTO up;
        END IF;
    END IF;
    IF changedefaultmp THEN
        SELECT into mp_id updateDefaultMealplan(_userid, mp_name);
    END IF;
    return up;
END
$$ LANGUAGE PLPGSQL;

This is the wrapper function where you can see the query generated for different inputs:

DROP FUNCTION IF EXISTS T ();
CREATE OR REPLACE FUNCTION T ()
RETURNS setof userprofile AS $$
declare
    _keys text[];
    _values text[];

    _userid int := 1;
    _token text := 'beet';
begin
    _keys := ARRAY['email', 'password', 'username', 'measuresystem', 'blanksymbol', 'defaultmealplan'];
    _values := ARRAY['[email protected]', 'secret', 'myname', 'metric', '?', 'new'];
    --_keys := ARRAY['email', 'blanksymbol'];
    --_values := ARRAY['[email protected]', '[]'];
    return query
        SELECT * from setProfileDynamic(_userid, _token, _keys, _values);
end
$$ LANGUAGE PLPGSQL;

I realize it's a lot of code to get through, I hope the T function helps to clarify things. 'email' and 'password' params are not returning. 'defaultmealplan' triggers the second query. Any of 'username', 'measuresystem', 'blanksymbol' or 'defaultmealplan' should return a value into the userprofile type. Thanks for any forthcoming feedback.

1

1 Answers

1
votes

the basic issue is so your dynamic query doesn't returns all necessary columns, second issue - you probably expecting, but it is not valid expectation, so records are assigned with respecting field' names. But when you assign some values to some composite type, postgres dosn't check name - only order is important. So you have to use NULLs for filling gaps and return all field.

you can simplify your code with array concating

DECLARE _return_cols text[] = '{}';

BEGIN
  _return_cols := _return_cols || quote_ident('some_column');
  _return_cols := _return_cols || quote_ident('some_other_column');

  ...