unaccent() 不适用于 plpgsql 动态查询中的希腊字母

unaccent() does not work with Greek letters in plpgsql dynamic query

我使用 PostgreSQL 10 并且 运行 CREATE EXTENSION unaccent; 成功。我有一个包含以下内容的 plgsql 函数 whereText := 'lower(unaccent(place.name)) LIKE lower(unaccent())';

稍后,根据用户的选择,可能会在 whereText 中添加更多的条款。

whereText最终在查询中使用:

placewithkeys := '%'||placename||'%';
RETURN QUERY EXECUTE format('SELECT id, name FROM '||fromText||' WHERE '||whereText)
     USING  placewithkeys , event, date;

whereText := 'LOWER(unaccent(place.name)) LIKE LOWER(unaccent())'; 不起作用,即使我删除了 LOWER 部分。

我做了 select __my_function('Τζι'); 但我什么也没得到,即使我应该得到结果,因为在数据库中有名称 Τζίμα

如果我删除 unaccent 并保留 LOWER 它可以工作,但对口音无效:τζ 使 Τζίμα 恢复正常。 unaccent 似乎引起了问题。

我错过了什么?我怎样才能解决这个问题?

由于有关于语法和可能的 SQLi 的评论,我提供了整个函数定义,现在改为在希腊语中不区分重音和不区分大小写:

CREATE  FUNCTION __a_search_place
(placename text, eventtype integer, eventdate integer, eventcentury integer, constructiondate integer, constructioncentury integer, arstyle integer, artype integer)
RETURNS TABLE
(place_id bigint, place_name text, place_geom geometry) 
AS $$
DECLARE
selectText text;
fromText text;
whereText text;
usingText text; 
placewithkeys text;
BEGIN   
    fromText := '
    place
    JOIN cep ON place.id = cep.place_id
    JOIN event ON cep.event_id = event.id                     
    ';  
    whereText := 'unaccent(place.name) iLIKE  unaccent()';   
    placewithkeys := '%'||placename||'%';
    IF constructiondate IS NOT NULL OR constructioncentury IS NOT NULL OR arstyle IS NOT NULL OR artype IS NOT NULL THEN
        fromText := fromText || '
        JOIN construction ON cep.construction_id = construction.id
        JOIN construction_atype ON construction.id = construction_atype.construction_id
        JOIN construction_astyle ON construction.id = construction_astyle.construction_id
        JOIN atype ON atype.id = construction_atype.atype_id
        JOIN astyle ON astyle.id = construction_astyle.astyle_id  
        ';   
    END IF;    
    IF eventtype IS NOT NULL THEN
        whereText := whereText || 'AND event.type =  ';
    END IF;
    IF eventdate IS NOT NULL THEN
        whereText := whereText || 'AND event.date =  ';
    END IF;
    IF eventcentury IS NOT NULL THEN
        whereText := whereText || 'AND event.century =  ';
    END IF;    
    IF constructiondate IS NOT NULL THEN
        whereText := whereText || 'AND construction.date =  ';
    END IF;
    IF constructioncentury IS NOT NULL THEN
        whereText := whereText || 'AND construction.century =  ';
    END IF;
    IF arstyle IS NOT NULL THEN
        whereText := whereText || 'AND astyle.id =  ';
    END IF;
    IF artype IS NOT NULL THEN
        whereText := whereText || 'AND atype.id =  ';
    END IF;   
    whereText := whereText || '    
    GROUP BY place.id, place.geom, place.name
    ';    

    RETURN QUERY EXECUTE format('SELECT place.id, place.name, place.geom FROM '||fromText||' WHERE '||whereText)      
    USING  placewithkeys, eventtype, eventdate, eventcentury, constructiondate, constructioncentury, arstyle, artype ;

END;
$$
LANGUAGE plpgsql;

Postgres 12

unaccent() 现在也适用于希腊字母。变音符号已删除:

db<>fiddle here

Quoting the release notes:

Allow unaccent to remove accents from Greek characters (Tasos Maschalidis)



Postgres 11 或更早版本

unaccent() 还不适用于希腊字母。来电:

SELECT unaccent('
ἀ ἁ ἂ ἃ ἄ ἅ ἆ ἇ Ἀ Ἁ Ἂ Ἃ Ἄ Ἅ Ἆ Ἇ
ἐ ἑ ἒ ἓ ἔ ἕ         Ἐ Ἑ Ἒ Ἓ Ἔ Ἕ     
ἠ ἡ ἢ ἣ ἤ ἥ ἦ ἧ Ἠ Ἡ Ἢ Ἣ Ἤ Ἥ Ἦ Ἧ
ἰ ἱ ἲ ἳ ἴ ἵ ἶ ἷ Ἰ Ἱ Ἲ Ἳ Ἴ Ἵ Ἶ Ἷ
ὀ ὁ ὂ ὃ ὄ ὅ         Ὀ Ὁ Ὂ Ὃ Ὄ Ὅ     
ὐ ὑ ὒ ὓ ὔ ὕ ὖ ὗ     Ὑ   Ὓ   Ὕ   Ὗ
ὠ ὡ ὢ ὣ ὤ ὥ ὦ ὧ Ὠ Ὡ Ὢ Ὣ Ὤ Ὥ Ὦ Ὧ
ὰ ά ὲ έ ὴ ή ὶ ί ὸ ό ὺ ύ ὼ ώ     
ᾀ ᾁ ᾂ ᾃ ᾄ ᾅ ᾆ ᾇ ᾈ ᾉ ᾊ ᾋ ᾌ ᾍ ᾎ ᾏ
ᾐ ᾑ ᾒ ᾓ ᾔ ᾕ ᾖ ᾗ ᾘ ᾙ ᾚ ᾛ ᾜ ᾝ ᾞ ᾟ
ᾠ ᾡ ᾢ ᾣ ᾤ ᾥ ᾦ ᾧ ᾨ ᾩ ᾪ ᾫ ᾬ ᾭ ᾮ ᾯ
ᾰ ᾱ ᾲ ᾳ ᾴ   ᾶ ᾷ Ᾰ Ᾱ Ὰ Ά ᾼ ᾽ ι ᾿
῀ ῁ ῂ ῃ ῄ   ῆ ῇ Ὲ Έ Ὴ Ή ῌ ῍ ῎ ῏
ῐ ῑ ῒ ΐ         ῖ ῗ Ῐ Ῑ Ὶ Ί     ῝ ῞ ῟
ῠ ῡ ῢ ΰ ῤ ῥ ῦ ῧ Ῠ Ῡ Ὺ Ύ Ῥ ῭ ΅ `
        ῲ ῳ ῴ   ῶ ῷ Ὸ Ό Ὼ Ώ ῼ ´ ῾ ');

... returns 所有字母都没有改变,没有像我们预期的那样删除变音符号。
(我从 Wikipedia page on Greek diacritics 中提取了这个列表。)

db<>fiddle here

看起来像 unaccent module 的缺点。您可以扩展默认的 unaccent 字典或创建您自己的字典。说明书上有说明。我过去创建了几本词典,很简单。你不是首先需要这个:

希腊字符的 Postgres 无重音规则:

用于 Postgres 9.6 的非重音规则加上希腊字符:

不过,您需要对服务器文件系统的写入权限 - 包含 unaccent 文件的目录。所以,在大多数云服务上是不可能的...

或者您可以 report a bug 并要求包括希腊变音符号。

旁白:动态 SQL 和 SQLi

您提供的代码片段易受SQL注入攻击。 </code> 连接为文字字符串,仅在稍后的 <code>EXECUTE 命令中解析,其中值通过 USING 子句安全传递。所以,那里没有不安全的串联。不过,我会这样做:

RETURN QUERY EXECUTE format(
   $q$
   SELECT id, name
   FROM   place ... 
   WHERE  lower(unaccent(place.name)) LIKE '%' || lower(unaccent()) || '%'
   $q$
   )
USING  placename, event, date;

备注:

  • 减少混淆 - 你原来在评论中甚至混淆了 Pavel,该领域的专业人士。

  • plpgsql 中的赋值稍微昂贵(比其他 PL 中的更高),因此采用赋值较少的编码风格。

  • LIKE 的两个 % 符号直接连接到主查询中,为查询规划器提供模式未锚定到开始或结束的信息,可能有助于制定更有效的计划。只有用户输入(安全地)作为变量传递。

  • 由于您的 WHERE 子句引用了 table place,因此 FROM 子句无论如何都需要包含此 table。因此,您不能一开始就独立地连接 FROM 子句。最好将所有内容保存在一个 format().

  • 使用美元引号,这样您就不必另外转义单引号了。

    • Insert text with single quotes in PostgreSQL
    • What are '$$' used for in PL/pgSQL
  • 也许 只需使用 ILIKE 而不是 lower(...) LIKE lower(...)。如果你使用三字母索引(对于这个查询来说似乎是最好的):那些也适用于 ILIKE

    • LOWER LIKE vs iLIKE
  • 我假设您知道您可能需要转义 LIKE 模式中具有特殊含义的字符?

    • How to escape string while matching pattern in PostgreSQL
    • Escape function for regular expression or LIKE patterns

审核函数

在您提供完整的功能之后...

CREATE OR REPLACE FUNCTION __a_search_place(
        placename             text
      , eventtype             int = NULL
      , eventdate             int = NULL
      , eventcentury          int = NULL
      , constructiondate      int = NULL
      , constructioncentury   int = NULL
      , arstyle               int = NULL
      , artype                int = NULL)
  RETURNS TABLE(place_id bigint, place_name text, place_geom geometry) AS
$func$
BEGIN
   -- RAISE NOTICE '%', concat_ws(E'\n' -- to debug
   RETURN QUERY EXECUTE concat_ws(E'\n'
 ,'SELECT p.id, p.name, p.geom
   FROM   place p
   WHERE  unaccent(p.name) ILIKE (''%'' || unaccent() || ''%'')'  -- no $-quotes
              -- any input besides placename ()
, CASE WHEN NOT (,,,,,,) IS NULL THEN
  'AND    EXISTS (
      SELECT
      FROM   cep
      JOIN   event e ON e.id = cep.event_id' END
               -- constructiondate, constructioncentury, arstyle, artype
 , CASE WHEN NOT (,,,) IS NULL THEN

     'JOIN   construction    con ON cep.construction_id = con.id
      JOIN   construction_atype  ON con.id = construction_atype.construction_id
      JOIN   construction_astyle ON con.id = construction_astyle.construction_id' END
              -- arstyle, artype
, CASE WHEN NOT (,) IS NULL THEN
     'JOIN   atype               ON atype.id = construction_atype.atype_id
      JOIN   astyle              ON astyle.id = construction_astyle.astyle_id' END
 , CASE WHEN NOT (,,,,,,) IS NULL THEN
     'WHERE  cep.place_id = p.id' END
 , CASE WHEN eventtype           IS NOT NULL THEN 'AND e.type = '      END
 , CASE WHEN eventdate           IS NOT NULL THEN 'AND e.date = '      END
 , CASE WHEN eventcentury        IS NOT NULL THEN 'AND e.century = '   END
 , CASE WHEN constructiondate    IS NOT NULL THEN 'AND con.date = '    END
 , CASE WHEN constructioncentury IS NOT NULL THEN 'AND con.century = ' END
 , CASE WHEN arstyle             IS NOT NULL THEN 'AND astyle.id = '   END
 , CASE WHEN artype              IS NOT NULL THEN 'AND atype.id = '    END
 , CASE WHEN NOT (,,,,,,) IS NULL THEN
     ')' END
   );
   USING  placename
        , eventtype
        , eventdate
        , eventcentury
        , constructiondate
        , constructioncentury
        , arstyle
        , artype;
END
$func$  LANGUAGE plpgsql;

这是 完全重写,有几处改进。应该使功能大幅提升。还有 SQLi-safe(就像你原来的一样)。应该在功能上相同 除了 我加入较少 table 的情况,这可能不会过滤通过加入过滤的行table一个人。

主要特点:

  • 使用 EXISTS() 而不是在外层加上 GROUP BY 的大量连接。这有助于实现更好的性能。相关:

    • Search a JSON array for an object containing a value matching a pattern
  • format() 通常是连接用户输入的 SQL 的不错选择。但是由于您封装了所有代码元素并且只传递标志,因此在这种情况下您不需要它。相反,concat_ws() 是有帮助的。相关:

    • How to concatenate columns in a Postgres SELECT?
  • 仅连接您实际需要的 JOIN。

  • 更少的赋值,更短的代码。

  • 参数的默认值。允许缺少参数的简化调用。喜欢:

    SELECT __a_search_place('foo', 2, 3, 4);
    SELECT __a_search_place('foo');
    

    相关:

    • Optional argument in PL/pgSQL function
  • 关于用于测试任何值是否为 NOT NULL 的简短 ROW() 语法:

    • Why is IS NOT NULL false when checking a row type?