4
  1. Is it possible to do something like the following with SQL, not PL/pgSQL (note if it's only possible with PL/pgSQL, then how)?

    IF password = 'swordfish' THEN
        SELECT a, b, c FROM users;
    ELSE
        SELECT -1; -- unauthorized error code
    END IF;
    
  2. Ideally, could I wrap the above in a function with TRUE being an argument?

  3. Rather, is it possible to set the command status string to -1?

I'm asking this because I want the query to return an error code, like -1, if someone tries to get a list of all the users with the wrong password. This is for a web app with user accounts that each have a password. So, this is not something I want to manage with database roles/permissions.

ma11hew28
  • 121,420
  • 116
  • 450
  • 651
  • What would TRUE really look like? Also, you're returning a different number of columns in the two branches which won't work. – Trygve Laugstøl Apr 02 '14 at 13:18
  • @trygvis I've updated my question. OK. Maybe should change the second `SELECT` to `-1, NULL, NULL`? – ma11hew28 Apr 02 '14 at 13:20
  • @trygvis a simple `SELECT` would not return to anywhere. The provided example is valid pl/pgsql, just does nothing. – pozs Apr 02 '14 at 13:23
  • @MattDiPasquale I'm guessing that in your case the SQL idiomatic way would be to do something `SELECT a,b,c FROM users WHERE password='..'`and check the number of rows you got back. – Trygve Laugstøl Apr 02 '14 at 13:27
  • @pozs it was implied that this was in a non-PL/pgSQL query in the question. – Trygve Laugstøl Apr 02 '14 at 13:28
  • @trygvis `IF` is not valid in non pl/pgsql at all. Maybe i'm misunderstood, but i think his question is how to produce something like in the example in SQL, where the example *is* in pl/pgsql. – pozs Apr 02 '14 at 13:30
  • @trygvis I thought of `SELECT ... WHERE password =`, but I need to be able to distinguish between these two cases: 1. Wrong password, and 2. There are actually no users in the table yet. And, that solution doesn't do that. – ma11hew28 Apr 02 '14 at 13:32
  • @MattDiPasquale that's true, hm – Trygve Laugstøl Apr 02 '14 at 13:36
  • @trygvis another idea I had was to initialize `users` with a row with `id = -1` (`id` is `serial`) and then just use something like `... WHERE CASE WHEN FALSE id = -1`. But, that seems kind of funky. – ma11hew28 Apr 02 '14 at 13:37
  • Surely you want to `RAISE EXCEPTION ...` if the user has the wrong "password" rather than returning some dummy value? Also: Remember, *the user can read the source code of your procedure from `pg_proc`*. – Craig Ringer Apr 03 '14 at 02:16

4 Answers4

3

Algorithm

  1. Select 1 into a (authorized) if we find a user_id_1-session_id match.
  2. Select 0, NULL, NULL into u (unauthorized) if we didn't find a match in step 1.
  3. Select user_id, body, sent into s (select) if we did find a match in step 1.
  4. Union u and s.

Code

-- List messages between two users with `user_id_1`, `session_id`, `user_id_2`
CREATE FUNCTION messages(bigint, uuid, bigint) RETURNS TABLE(i bigint, b text, s double precision) AS
$$
    WITH a AS (
        SELECT 1
        FROM sessions
        WHERE user_id = $1
        AND id = $2
    ), u AS (
        SELECT 0, NULL::text, NULL::double precision
        WHERE NOT EXISTS (SELECT 1 FROM a)
    ), s AS (
        SELECT user_id, body, trunc(EXTRACT(EPOCH FROM sent))
        FROM messages
        WHERE EXISTS (SELECT 1 FROM a)
        AND chat_id = pair($1, $3)
        LIMIT 20
    )
    SELECT * FROM u UNION ALL SELECT * FROM s;
$$
LANGUAGE SQL STABLE;
ma11hew28
  • 121,420
  • 116
  • 450
  • 651
1

The PL/pgsql function below returns the messages sent between user_id & with_user_id if the user_id:key pair is authorized, as determined by the user-defined function (UDF) user_auth. Otherwise, it returns one row with from = -1 . The other UDF, pair, is a unique unordered pairing function that, given two user IDs, returns the chat_id to which the messages belong.

--- Arguments: user_id, key, with_user_id
CREATE FUNCTION messages(bigint, uuid, bigint)
RETURNS TABLE(from bigint, body text, sent double precision) AS $$
BEGIN
    IF user_auth($1, $2) THEN
        RETURN QUERY SELECT from, body, trunc(EXTRACT(EPOCH FROM sent))
                     FROM messages WHERE chat_id = pair($1, $3);
    ELSE
        i := -1;
        RETURN NEXT;
    END IF;
END;
$$ LANGUAGE plpgsql STABLE;

I don't know how to translate this to an SQL function or whether that would be better.

ma11hew28
  • 121,420
  • 116
  • 450
  • 651
  • variable `i` is not defined in that scope, and `RETURN NEXT;` does not return anything (an empty result set will be the result in the `ELSE` branch) - use something like `RETURN NEXT ROW(-1, 'auth error', EXTRACT(EPOCH FROM CURRENT_TIMESTAMP));` – pozs Apr 03 '14 at 08:31
0

There is a CASE expression in addition to the (pl/pgsql only) CASE control structure.

EDIT: CASE expression in sql context:

SELECT CASE
           WHEN my_conditions_are_met THEN a
           ELSE NULL
       END AS a_or_null,
       b,
       c
FROM users;

EDIT 2: given your example that's how you can do it in pure SQL:

WITH params AS (
    SELECT user_auth(:user_id, :key)     AS user_auth,
           pair(:user_id, :with_user_id) AS chat_id
), error_message AS (
    SELECT -1                                    AS "from",
           'auth error'                          AS "body",
           EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) AS "sent"
)
SELECT from, body, trunc(EXTRACT(EPOCH FROM sent))
FROM messages
JOIN params ON messages.chat_id = params.chat_id AND params.user_auth
UNION ALL
SELECT error_message.*
FROM error_message
JOIN params ON NOT params.user_auth
Community
  • 1
  • 1
pozs
  • 34,608
  • 5
  • 57
  • 63
  • I know, but how would you use it to do what I'm asking? – ma11hew28 Apr 02 '14 at 13:28
  • This is a pain if you need to make sure that all the values are `NULL`, but it'll work nicely if only the error code needs to have a particular value. – Trygve Laugstøl Apr 02 '14 at 13:35
  • It is. But why return an error code like the normal result at all? All clients must have some form of error-handling, affected row counting, which is way more better. – pozs Apr 02 '14 at 13:37
  • @pozs this is a neat idea, but it returns all of the users (just with all their `a`s as `NULL`) even if `NOT my_conditions_are_met`. My actual query is much more expensive than `SELECT a, b, c FROM users`, and so, I'd like to avoid executing it. – ma11hew28 Apr 02 '14 at 13:49
  • Perhaps I should just have the client make the first query to check if `my_conditions_are_met`, and if they are, then the client makes the actual query. I just wanted to do it all in one shot and avoid making superfluous roundtrips to the database server. – ma11hew28 Apr 02 '14 at 13:51
  • Otherwise, is a better way possible with PL/pgSQL? – ma11hew28 Apr 02 '14 at 13:54
  • That depends - what are you really want to achieve? Why is an `IF` in your original example? – pozs Apr 02 '14 at 13:55
  • @pozs I only want to make the query if the client passes the correct password (or access token). Let's say you want to see your messages on Facebook. Facebook should only show them to you if you pass the correct `access_token`. Otherwise, Facebook should return an error. That's what I want to do. It's for a RESTful API. – ma11hew28 Apr 02 '14 at 14:32
  • Then rather your server side code should contain the condition, not the db query. – pozs Apr 02 '14 at 15:22
  • @pozs Why? I think [I figured out](http://stackoverflow.com/a/22816804/242933) how to do what I wanted with PL/pgSQL, thereby saving an extra roundtrip to the database server. – ma11hew28 Apr 02 '14 at 15:56
0

This will work, but it's not pretty:

WITH 
  u AS (SELECT * FROM user WHERE mail = '..'),
  code AS (
    SELECT 
      CASE (SELECT count(*) FROM u)
      WHEN 0 THEN
        'not found'
      ELSE
        CASE (SELECT count(*) FROM u WHERE password = '..')
        WHEN 1 THEN
          'right password'
        ELSE
          'wrong password'
        END
      END)
SELECT
  code.*,
  u.*
FROM code NATURAL LEFT OUTER JOIN u

I think you might want to look into creating a result set returning function instead.

Trygve Laugstøl
  • 7,440
  • 2
  • 36
  • 40
  • Interesting... Thanks. Still, this returns the code with every row. Is there a way to do it with PL/pgSQL where the function will either return just the error code or the results, but not both? – ma11hew28 Apr 02 '14 at 14:38
  • I think [I figured out](http://stackoverflow.com/a/22816804/242933) how to do what I wanted with PL/pgSQL. – ma11hew28 Apr 02 '14 at 15:54
  • 1
    @MattDiPasquale yes, that's probably the best solution. – Trygve Laugstøl Apr 02 '14 at 16:11