2

Transactions column's names in below code are dynamicaly generated (so it means that sometimes particular name/column doesn't exist). Using this select it finishes successfully only in case when every of those names exists, if not, I got error like this (example):

Error(s), warning(s): 42703: column "TransactionA" does not exist

SELECT  
             *,
               ((CASE WHEN "TransactionA" IS NULL THEN 0 ELSE "TransactionA" END) - 
               (CASE WHEN "TransactionB" IS NULL THEN 0 ELSE "TransactionB" END) +
               (CASE WHEN "TransactionC" IS NULL THEN 0 ELSE "TransactionC" END)) AS "Account_balance"
        FROM Summary ORDER BY id;

Could you tell me please how can I check first if the column exists and then how can I nest another CASE statement or other condition statement to make it working in a correct way?

Gordon Linoff
  • 1,242,037
  • 58
  • 646
  • 786
Rafau YT
  • 21
  • 1
  • 4
  • `COALESCE()`will greatly simplify your expression.BTW: what do you mean by `...if a column exists...` – wildplasser Aug 04 '19 at 20:03
  • But using just COALESCE() there's still error appearing that the column doesn't exist. By saying "if a column exists" I mean it's generated in some function and not every time the column names are the same (it's always 5 columns, but 2 columns can have a few different names - I know what names they can have, but it's randomly chosen, so 1 time it can be TransactionX and other time it can be TransactionY. – Rafau YT Aug 05 '19 at 13:54
  • You can only use existing column names. But you may need `COALESCE` *on top of that* to catch `NULL` values from existing columns. – Erwin Brandstetter Aug 07 '19 at 01:23

3 Answers3

1

You can build any query dynamically with information from the Postgres catalog tables. pg_attribute in your case. Alternatively, use the information schema. See:

Basic query to see which of the given columns exist in a given table:

SELECT attname
FROM   pg_attribute a
WHERE  attrelid = 'public.summary'::regclass  --  tbl here
AND    NOT attisdropped
AND    attnum > 0
AND    attname IN ('TransactionA', 'TransactionB', 'TransactionC');  -- columns here

Building on this, you can have Postgres generate your whole query. While being at it, look up whether columns are defined NOT NULL, in which case they don't need COALESCE:

CREATE OR REPLACE FUNCTION f_build_query(_tbl regclass, _columns json)
  RETURNS text AS
$func$
DECLARE
   _expr text;
BEGIN

SELECT INTO _expr
       string_agg (op || CASE WHEN attnotnull
                              THEN quote_ident(attname)
                              ELSE format('COALESCE(%I, 0)', attname) END
                 , '')
FROM  (
   SELECT j->>'name' AS attname
        , CASE WHEN j->>'op' = '-' THEN ' - ' ELSE ' + ' END AS op
   FROM   json_array_elements(_columns) j
   ) j
JOIN   pg_attribute a USING (attname)
WHERE  attrelid = _tbl
AND    NOT attisdropped
AND    attnum > 0;

IF NOT FOUND THEN
   RAISE EXCEPTION 'No column found!'; -- or more info
END IF;

RETURN
'SELECT *,' || _expr || ' AS "Account_balance"
FROM   ' || _tbl || '
ORDER  BY id;';

END
$func$  LANGUAGE plpgsql;

The table itself is parameterized, too. May or may not be useful for you. The only assumption is that every table has an id column for the ORDER BY. Related:

I pass columns names and the associated operator as JSON document for flexibility. Only + or - are expected as operator. Input is safely concatenated to make SQL injection impossible.About json_array_elements():

Example call:

SELECT f_build_query('summary', '[{"name":"TransactionA"}
                                , {"name":"TransactionB", "op": "-"}
                                , {"name":"TransactionC"}]');

Returns the according valid query string, like:

SELECT *, + COALESCE("TransactionA", 0) - COALESCE("TransactionB", 0) AS "Account_balance"
FROM   summary
ORDER  BY id;

"TransactionC" isn't there in this case. If both existing columns happen to be NOT NULL, you get instead:

SELECT *, + "TransactionA" - "TransactionB" AS "Account_balance"
FROM   summary
ORDER  BY id;

db<>fiddle here

You could execute the generated query in the function immediately and return result rows directly. But that's hard as your return type is a combination of a table rows (unknown until execution time?) plus additional column, and SQL demands to know the return type in advance. For just id and sum (stable return type), it would be easy ...

It's odd that your CaMeL-case column names are double-quoted, but the CaMeL-case table name is not. By mistake? See:

How to pass column names containing single quotes?

Addressing additional question from comment.

If someone used column names containing single quotes by mistake:

CREATE TABLE madness (
  id int PRIMARY KEY
, "'TransactionA'" numeric NOT NULL  -- you wouldn't do that ...
, "'TransactionC'" numeric NOT NULL
);

For the above function, the JSON value is passed as quoted string literal. If that string is enclosed in single-quotes, escape contained single-quotes by doubling them up. This is required on top of valid JSON format:

SELECT f_build_query('madness', '[{"name":"''TransactionA''"}
                                , {"name":"TransactionB", "op": "-"}
                                , {"name":"TransactionC"}]');  -- 

("''TransactionA''" finds a match, "TransactionC" does not.)

Or use dollar quoting instead:

SELECT f_build_query('madness', $$[{"name":"'TransactionA'"}
                                , {"name":"TransactionB", "op": "-"}
                                , {"name":"TransactionC"}]$$);

db<>fiddle here with added examples

See:

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • Could you tell me how can I write column name which has single quotes, please? I mean while calling function f_build_query. It looks like this: {"name":"TransactionA"}, but what if I have column name which is named like: 'TransactionA' and not like I said before: TransactionA? I know it makes no sense to use quotes in name of column, but in this case I can't change it – Rafau YT Aug 05 '19 at 13:58
  • @RafauYT: Consider the addendum above. – Erwin Brandstetter Aug 05 '19 at 22:09
  • [So do you have your answer?](https://meta.stackexchange.com/a/5235/169168) – Erwin Brandstetter Aug 07 '19 at 01:25
0

Assuming that id is a unique id in summary, then you can use the following trick:

SELECT s.*,
       (COALESCE("TransactionA", 0) -
        COALESCE("TransactionB", 0) +
        COALESCE("TransactionC", 0)
       ) AS Account_balance
FROM (SELECT id, . . .   -- All columns except the TransactionX columns
FROM (SELECT s.*, 
             (SELECT TransactionA FROM summary s2 WHERE s2.id = s.id) as TransactionA,
             (SELECT TransactionB FROM summary s2 WHERE s2.id = s.id) as TransactionB,
             (SELECT TransactionC FROM summary s2 WHERE s2.id = s.id) as TransactionC
            FROM Summary s
           ) s CROSS JOIN
           (VALUES (NULL, NULL, NULL)) v(TransactionA, TransactionB, TransactionC)
      ) s
ORDER BY s.id;

The trick here is that the correlated subqueries do not qualify the TransactionA. If the value is defined for summary, then that will be used. If not, it will come from the values() clause in the outer query.

This is a bit of a hack, but it can be handy under certain circumstances.

Gordon Linoff
  • 1,242,037
  • 58
  • 646
  • 786
  • Could you tell me please, if there should be 2 tables: summary and Summary? If yes, why? And what means 'v' letter in front of brackets which include columns' names (transactions) inside? – Rafau YT Aug 05 '19 at 13:50
  • @RafauYT . . . I have no idea why you are asking about two tables. Identifiers in SQL are usually case-insensitive. The `v` is a table alias. – Gordon Linoff Aug 05 '19 at 14:16
  • I understand. Could you tell me please, how can I type columns' names to your code, if the names are with sinqle quotes? The names look like this: 'Transaction A'. Does in COALESCE should it look like this?: "'Transaction A'" and with others like this?: 'Transaction A'? Sorry if the questions are not so smart, but I was trying many possibilities and nothing works yet. – Rafau YT Aug 05 '19 at 17:46
  • @RafauYT . . . Column names should *never* be in single quotes. *Only* use single quotes for string and date literals. Use double quotes for identifiers that need to be escaped -- or better yet, use names that don't need to be escaped. – Gordon Linoff Aug 05 '19 at 17:59
0

Check this example:

UPDATE yourtable1 
SET yourcolumn = (
    CASE 
        WHEN setting.value IS NOT NULL
            THEN CASE WHEN replace(setting.value,'"','') <> '' THEN replace(setting.value,'"','') ELSE NULL END
        ELSE NULL
    END
)::TIME FROM (SELECT value FROM yourtable2 WHERE key = 'ABC') AS setting;