2

This simple function returns an ordered list of title strings.

create or replace function testfunction1 ()
returns table (
  id        bigint,
  lang_code tlang_code,
  title     varchar
)
stable language sql as $$
  select 
    id, lang_code, title
  from 
    testable
  order by
    title collate "es_ES";
$$;

select * from testfunction ();

id|lang_code|title           |
--|---------|----------------|
12|DE       |NOCH FESTZULEGEN|
16|DE       |NOCH FESTZULEGEN|
 8|DE       |NOCH FESTZULEGEN|
14|ES       |POR DETERMINAR  |
 6|ES       |POR DETERMINAR  |
10|ES       |POR DETERMINAR  |
 5|EN       |TO BE DETERMINED|
 9|EN       |TO BE DETERMINED|
13|EN       |TO BE DETERMINED|
11|FR       |À DÉTERMINER    |
15|FR       |À DÉTERMINER    |
 7|FR       |À DÉTERMINER    |

However, when I try to introduce a collation order with collate I am not being able to get the syntax right to set the proper collation order based on the parameter _lang_code.

create or replace function testfunction2 (_lang_code tlang_code)
returns table (
    id        bigint,
    lang_code tlang_code,
    title     varchar
)
stable language sql as $$
    select 
        id, lang_code, title
    from 
        testable
    where 
        lang_code = _lang_code
    order by
        title collate 
            case _lang_code
                when 'EN' then "en_US" 
                when 'ES' then "es_ES"
                when 'FR' then "fr_FR"
                when 'DE' then "de_DE"
            end asc;
$$;

The error is SQL Error [42601]: ERROR: syntax error at or near "case".

I have unsuccessfully tried positioning the case everywhere in the order by clause. Maybe the "en_US" is not considered a scalar value?


EDIT I've added where lang_code = _lang_code after Laurenz Albe comment. This was a missing clause when traslating from my real problem to this simplified example.

However the issue with the case remains with the same SQL error.


SOLUTION

As @Lorenz Albe pointed in a comment, "en_US" is an identifier, not a scalar value. This prevents the case-when structure from returning it in any of its when branches. So no SQL way happens to exist.

As a work around, dynamic SQL from @doctore or moving the case to embrace the whole sentence, are both inelegant but functional solutions to the problem.

coterobarros
  • 353
  • 4
  • 15
  • You are trying to mix different `collate` values for the same ordination process, that is basically the reason why you "first query" works (it only has one) but not the next tries. You need to change the approach and use the suitable `collate` taking into account the `lang_code` value and "mix the different chunks" in the way you want (one "chunk" for every language). – doctore Sep 11 '20 at 10:49
  • I am not sure I understand your comment. Clearly I do not want to mix collation orders, I just want to select one or the other depending on the `_lang_code` parameter. This is clearly a feature the `collate` clause introduced in Postgre12 (?). Not sure what you mean by "chunks" – coterobarros Sep 11 '20 at 10:52
  • I could switch the whole select sentence from the beginning but loosing the benefit of having an SQL function. – coterobarros Sep 11 '20 at 10:53
  • The approach of Akhilesh Mishra leads to "mix" or set twice the collation order, but that's just an artifact from the syntax error. – coterobarros Sep 11 '20 at 10:55
  • What seems to me is that the parser treats `collate "en_US"` as a whole single thing, i.e., `"en_US"` is not being considered a scalar parameter of `collate`. – coterobarros Sep 11 '20 at 10:57
  • PostgreSQL allows you define different collation for different columns and every one will be used if you order such ones. But in your example you are "mixing concepts and functionalities", what you are trying to do is `using only one varchar column, let me order by a dynamic collation value`, that is not possible. So you have mainly two options: **1.** Include a column for every different `collate` (probably you wan't it). **2.** Order `EN` rows, order `ES` rows, etc every one in a different query (or subquery) and merge the results in only "final one" – doctore Sep 11 '20 at 11:01
  • Yes, only one collation order can be set on a varchar at DDL time (`create table`) but I am building a `select` sentence, not declaring a table. You can choose any collation in your `order by` clause, independently of the one you've chosen when declaring the column. – coterobarros Sep 11 '20 at 11:06
  • Yes, you can choose a different `collation`, but only one for the "whole" ordination process – doctore Sep 11 '20 at 11:07
  • "Character-string data is sorted according to the collation that applies to the column being sorted. That can be overridden at need by including a COLLATE clause in the expression, for example ORDER BY mycolumn COLLATE "en_US". For more information see Section 4.2.10 and Section 23.2." from https://www.postgresql.org/docs/12/sql-select.html – coterobarros Sep 11 '20 at 11:09
  • My real case does not eve use default collation orders but nondeterministic custom ones. I test this every day with 4-language collation orders, as in the example. – coterobarros Sep 11 '20 at 11:10
  • For an specific column, you can use the default `collation` value or a different one. But you can use only one, not: `collation1`, `collation2`, `collation3`, etc in the same query for the same column. I have tried to explain the problem of your approach but, of course, feel free to continue trying to follow the same one – doctore Sep 11 '20 at 11:16
  • Ok, I guess you are probably missing the underscore before `lang_code`? Keep an eye on it, `_lang_code` is a function parameter, so every time the function is executed it has only one value, let's say 'DE'. In that case the `case-when` control structure should applied **only one** collation order, the german one. – coterobarros Sep 11 '20 at 11:19
  • The logic of the function is according to the functionality of PostgreSQL `collate` but the syntax is not being accepted for some other reason. Thank you for your time anyway – coterobarros Sep 11 '20 at 11:21
  • 1
    Ok, I understand now the "whole flow". That is the reason I have included an answer to allow you deal with your use case – doctore Sep 11 '20 at 11:55

3 Answers3

1

Taking into account you are using the parameter _lang_code to choose the "internal language" to filter. The following PL/SQL code allow you change the collate in the final query dynamically:

create or replace function testfunction2 (_lang_code varchar)
returns table (
                  id        bigint,
                  lang_code varchar,
                  title     varchar
              )
language plpgsql
as $$
declare
  final_collate varchar;
  final_query varchar;
begin
  if (_lang_code = 'EN') then
    final_collate := 'en_US';
  elsif (_lang_code = 'ES') then
    final_collate := 'es_ES';
  end if;
  -- Include other use cases you need

  final_query := 'select t.id, t.lang_code, t.title ' ||
                 'from test_table t ' ||
                 'where t.lang_code = ''' || _lang_code || ''' ' ||
                 'order by t.title collate "' || final_collate || '" asc';

  --raise exception 'Final query: %', final_query;

  return query
    execute final_query;
end;$$

Now you can execute your tests or even uncomment the raise exception to be sure about the suitable "final query":

select testfunction2('EN')
select testfunction2('ES')

PD: I have changed the type of _lang_code and lang_code to varchar because I assume tlang_code is a custom one.

doctore
  • 3,295
  • 1
  • 23
  • 39
  • Thanks doctore. Sorry I should have exchanged `tlang_code` by `char(2)`. In fact it is a domain of type char(2) with checks. Unfortunately this solution is PL/PGSQL, equivalent to doing a switch case from the beginning. – coterobarros Sep 11 '20 at 11:55
  • 1
    If no way to use `pl/sql` then we are like "in the beginning" of our debate, and as @LaurenzAlbe has told you, probably what you are trying to do is not possible. – doctore Sep 11 '20 at 12:01
  • Well, Laurenz outlined a logic error of mine that was fixed, but did not approach the original syntax error in the case-when structure. – coterobarros Sep 11 '20 at 12:04
  • I am still open to understand and fix that syntax error! – coterobarros Sep 11 '20 at 12:04
  • 1
    You cannot include "anything" between `order by` and `case` clauses, however, as you have discovered with @AkhileshMishra's answer, if you remove your `title collate` PostgreSQL notifies you another error, what basically means, `you cannot use more than one collate value in the order by clause for the same column` – doctore Sep 11 '20 at 12:20
  • 1
    I chose your solution as the one to the question, however a `where lang_code = _lang_code` must be added to the `final_query`. I tried to edit but the queue is full. Thank you – coterobarros Sep 11 '20 at 12:28
0

Write your case for order by like below:

create or replace function testfunction2 (_lang_code tlang_code)
returns table (
    id        bigint,
    lang_code tlang_code,
    title     varchar
)
stable language sql as $$
    select 
        id, lang_code, title
    from 
        testtable
    order by  
            case _lang_code
                when 'EN' then title collate "en_US" 
                when 'ES' then title collate "es_ES"
                when 'FR' then title collate "fr_FR"
                when 'DE' then title collate "de_DE"
            end asc;
$$;
Akhilesh Mishra
  • 4,830
  • 3
  • 12
  • 29
  • Doesn't work. As I said, I tested (almost) everything :-) The error now is `SQL Error [42P21]: ERROR: collation mismatch between explicit collations "en_US" and "es_ES"` – coterobarros Sep 11 '20 at 09:53
  • Its a different problem. Above solution is only for syntax error you have mentioned in the question. – Akhilesh Mishra Sep 11 '20 at 09:54
  • It's the same problem. The parser expects a scalar after `when`, not part of a clause – coterobarros Sep 11 '20 at 09:55
  • See https://www.postgresql.org/docs/12/functions-conditional.html#FUNCTIONS-CASE "The data types of all the result expressions must be convertible to a single output type". – coterobarros Sep 11 '20 at 10:00
  • I even tried enclosing `title collate "en_US"` between parenthesis – coterobarros Sep 11 '20 at 10:01
0

@doctore solution forces a PL/PGSQL function, so does this another approach of moving the case-when to embrace the whole select sentence. They both are far from elegant, but prove the question had a sense.

Unfortunately, I haven't found the cause of the syntax error in my original function.

create or replace function testfunction3 (_lang_code char(2))
returns table (
  id        bigint,
  lang_code char(2),
  title     varchar
)
stable language plpgsql as $$
begin
  case _lang_code
    when 'EN' then
      return query
      select *
      from testtable t 
      where t.lang_code = _lang_code  
      order by t.title collate "en_US";
     
    when 'ES' then
      return query
      select *
      from testtable t 
      where t.lang_code = _lang_code  
      order by t.title collate "es_ES";

    when 'FR' then
      return query
      select *
      from testtable t 
      where t.lang_code = _lang_code  
      order by t.title collate "fr_FR";

    when 'DE' then
      return query
      select *
      from testtable t 
      where t.lang_code = _lang_code  
      order by t.title collate "de_DE";
     
   end case;
end
$$;
coterobarros
  • 353
  • 4
  • 15