0

This is the query:

SELECT DISTINCT 
         completed_phases,
         CAST(completed_phases::bit(8) AS VARCHAR),
         CASE WHEN STRPOS(CAST(completed_phases::bit(8) AS VARCHAR),'1')=1 THEN 'FT' ELSE '' END ||
         CASE WHEN STRPOS(CAST(completed_phases::bit(8) AS VARCHAR),'1')=2 THEN 'ED' ELSE '' END ||
         CASE WHEN STRPOS(CAST(completed_phases::bit(8) AS VARCHAR),'1')=3 THEN 'MC' ELSE '' END ||
         CASE WHEN STRPOS(CAST(completed_phases::bit(8) AS VARCHAR),'1')=4 THEN 'HC' ELSE '' END ||
         CASE WHEN STRPOS(CAST(completed_phases::bit(8) AS VARCHAR),'1')=5 THEN 'UV' ELSE '' END ||
         CASE WHEN STRPOS(CAST(completed_phases::bit(8) AS VARCHAR),'1')=6 THEN 'TT' ELSE '' END ||
         CASE WHEN STRPOS(CAST(completed_phases::bit(8) AS VARCHAR),'1')=7 THEN 'RX' ELSE '' END ||
         CASE WHEN STRPOS(CAST(completed_phases::bit(8) AS VARCHAR),'1')=8 THEN 'PI' ELSE '' END
FROM rx_sales_order

If completed_phase is 129, my output for final column should be FTPI. But it is only showing FT. Only the first case statement seems to work, even if all of them are distinct.

2 Answers2

2

STRPOS() will always return the first occurance of the searched string. So all calls to strpos() will return 1 for the input value 129.

You can use substring() instead:

CASE WHEN substring(CAST(completed_phases::bit(8) AS VARCHAR),1,1)='1' THEN 'FT' ELSE '' END ||
CASE WHEN substring(CAST(completed_phases::bit(8) AS VARCHAR),2,1)='1' THEN 'ED' ELSE '' END ||
CASE WHEN substring(CAST(completed_phases::bit(8) AS VARCHAR),3,1)='1' THEN 'MC' ELSE '' END ||
CASE WHEN substring(CAST(completed_phases::bit(8) AS VARCHAR),4,1)='1' THEN 'HC' ELSE '' END ||
CASE WHEN substring(CAST(completed_phases::bit(8) AS VARCHAR),5,1)='1' THEN 'UV' ELSE '' END ||
CASE WHEN substring(CAST(completed_phases::bit(8) AS VARCHAR),6,1)='1' THEN 'TT' ELSE '' END ||
CASE WHEN substring(CAST(completed_phases::bit(8) AS VARCHAR),7,1)='1' THEN 'RX' ELSE '' END ||
CASE WHEN substring(CAST(completed_phases::bit(8) AS VARCHAR),8,1)='1' THEN 'PI' ELSE '' END

Another option would be to use get_bit() to test each bit individually:

case when get_bit(completed_phases::bit(8), 0) = 1 then 'FT' else '' END||
case when get_bit(completed_phases::bit(8), 1) = 1 then 'ED' else '' END||
case when get_bit(completed_phases::bit(8), 2) = 1 then 'MC' else '' END||
case when get_bit(completed_phases::bit(8), 3) = 1 then 'HC' else '' END||
case when get_bit(completed_phases::bit(8), 4) = 1 then 'UV' else '' END||
case when get_bit(completed_phases::bit(8), 5) = 1 then 'TT' else '' END||
case when get_bit(completed_phases::bit(8), 6) = 1 then 'RX' else '' END||
case when get_bit(completed_phases::bit(8), 7) = 1 then 'PI' else '' END

A more flexible way of doing that is to turn the bits into rows and use an array as a lookup. Something like:

with lookup (codes) as (
   values (array['FT','ED','MC','HC','UV','TT','RX','PI'])
)
SELECT  completed_phases, 
        completed_phases::bit(8),
        x.code
FROM rx_sales_order
  join lateral (
    select string_agg(codes[i],'') as code
    from lookup, unnest(string_to_array(completed_phases::bit(8)::text, null)) with ordinality as t(b,i) 
    where b = '1'
 ) as x on true

The part regexp_split_to_table(completed_phases::bit(8)::text, '') with ordinality as t(b,i) will return the following for the value 129:

b | i
--+--
1 | 1
0 | 2
0 | 3
0 | 4
0 | 5
0 | 6
0 | 7
1 | 8

code[i] the uses the index to lookup the matching code and string_agg() then puts all selected codes together again into a single string. The condition where b = '1' only selects the bits that are set.

That solution will be substantially slower than the hardcoded case expression (because it increases the number of rows, just to reduce them again) - but it is more flexible and easier to maintain.


If you need that a lot, the best option would be to put the case expression into a function and use the function in your queries.

create or replace function get_codes(p_phases integer)
  returns text
as 
$$
  select
    case when get_bit(p_phases::bit(8), 0) = 1 then 'FT' else '' END||
    case when get_bit(p_phases::bit(8), 1) = 1 then 'ED' else '' END||
    case when get_bit(p_phases::bit(8), 2) = 1 then 'MC' else '' END||
    case when get_bit(p_phases::bit(8), 3) = 1 then 'HC' else '' END||
    case when get_bit(p_phases::bit(8), 4) = 1 then 'UV' else '' END||
    case when get_bit(p_phases::bit(8), 5) = 1 then 'TT' else '' END||
    case when get_bit(p_phases::bit(8), 6) = 1 then 'RX' else '' END||
    case when get_bit(p_phases::bit(8), 7) = 1 then 'PI' else '' END
$$
language sql;

Then use:

SELECT DISTINCT 
         completed_phases,
         get_codes(completed_phases) as codes
FROM rx_sales_order
  • Variation of the last approach: `select string_agg(y,'') from unnest(string_to_array('10010110', null), '{FT,ED,MC,HC,UV,TT,RX,PI}'::text[]) as t(x,y) where x = '1';` – Abelisto Sep 19 '17 at 13:15
  • @Abelisto: thanks, I didn't know you can pass `null` as the delimiter for `string_to_array()` –  Sep 19 '17 at 13:17
1

As pointed out in the answer by a_horse_with_no_name, strpos will return the first occurrence of the searched string. At any rate, it's better to use get_bit instead of casting to VARCHAR to check if a bit is set. Also, instead of ||, you can use concat, which will handle nulls as blank strings. Your query could then be rewritten to:

SELECT DISTINCT 
     completed_phases,
     CAST(completed_phases::bit(8) AS VARCHAR),
     concat(CASE get_bit(completed_phases::bit(8), 0) WHEN 1 THEN 'FT' END,
            CASE get_bit(completed_phases::bit(8), 1) WHEN 1 THEN 'ED' END,
            CASE get_bit(completed_phases::bit(8), 2) WHEN 1 THEN 'MC' END,
            CASE get_bit(completed_phases::bit(8), 3) WHEN 1 THEN 'HC' END,
            CASE get_bit(completed_phases::bit(8), 4) WHEN 1 THEN 'UV' END,
            CASE get_bit(completed_phases::bit(8), 5) WHEN 1 THEN 'TT' END,
            CASE get_bit(completed_phases::bit(8), 6) WHEN 1 THEN 'RX' END,
            CASE get_bit(completed_phases::bit(8), 7) WHEN 1 THEN 'PI' END)
FROM rx_sales_order;

On a side note, if you have the option to do so, I would recommend changing your database schema to store the phases as individual boolean columns instead of using a bit map. See Any disadvantages to bit flags in database columns? for a good discussion of why.

markusk
  • 6,477
  • 34
  • 39
  • 1
    And to be more "hackery": `CASE WHEN (completed_phases::bit(8) & B'10000000')::int::bool THEN 'FT' END ...` – Abelisto Sep 19 '17 at 15:38