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

Posted

技术标签:

【中文标题】unaccent() 不适用于 plpgsql 动态查询中的希腊字母【英文标题】:unaccent() does not work with Greek letters in plpgsql dynamic query 【发布时间】:2018-09-25 11:17:09 【问题描述】:

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

稍后,根据用户的选择,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($1))'; 不起作用,即使我删除了 LOWER 部分。

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

如果我删除 unaccent 并保留 LOWER 它可以工作,但不适用于重音:τζ 会按原样返回 Τζίμαunaccent 似乎引起了问题。

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

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

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($1)';   
    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 = $2 ';
    END IF;
    IF eventdate IS NOT NULL THEN
        whereText := whereText || 'AND event.date = $3 ';
    END IF;
    IF eventcentury IS NOT NULL THEN
        whereText := whereText || 'AND event.century = $4 ';
    END IF;    
    IF constructiondate IS NOT NULL THEN
        whereText := whereText || 'AND construction.date = $5 ';
    END IF;
    IF constructioncentury IS NOT NULL THEN
        whereText := whereText || 'AND construction.century = $6 ';
    END IF;
    IF arstyle IS NOT NULL THEN
        whereText := whereText || 'AND astyle.id = $7 ';
    END IF;
    IF artype IS NOT NULL THEN
        whereText := whereText || 'AND atype.id = $8 ';
    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;

【问题讨论】:

嗨 slevin:那是什么语言?希腊语?既然我显然不会说,你能否提供一个带口音的单词列表以及没有口音的单词应该是什么样子?德语看起来效果很好select lower(unaccent('MÜNSTER')) like lower(unaccent('mÜNsTer')); 注意:你的代码存在sql注入漏洞 @JimJones 是的,这是希腊语。一般来说,所有元音都有一个简单的标记来表示音调/重音。所以 χαρτί, ήλιος, ξύλο, βάση 有口音,而 χαρτι, ηλιος, ξυλο, βαση 他们没有。注意 ι, η, υ, α 中缺少一个简单的标记。这对解决方案有何帮助? @PavelStehule 因为这部分placewithkeys := '%'||placename||'%'; 我假设?请详细说明。谢谢。 我认为问题在于 unaccent 不支持希腊语。我要求提供示例以便我可以测试,以防万一它是另一种语言。 【参考方案1】:

Postgres 12

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

db小提琴here

Quoting the release notes:

允许不重音从希腊字符中删除重音 (Tasos Maschalidis)


Postgres 11 或更早版本

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

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

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

db小提琴here

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

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

https://gist.github.com/jfragoulis/9914900

Postgres 9.6 的非重音规则和希腊字符:

https://gist.github.com/marinoszak/7d5d6a8670faae0f4589c2da988f2ba3

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

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

旁白:动态 SQL 和 SQLi

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

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

注意事项:

不那么令人困惑 - 您原来在 cmets 中甚至使 Pavel 感到困惑,他是该领域的专业人士。

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

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

由于您的WHERE 子句引用表placeFROM 子句无论如何都需要包含此表。因此,您不能从一开始就独立连接 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($1) || ''%'')'  -- no $-quotes
              -- any input besides placename ($1)
, CASE WHEN NOT ($2,$3,$4,$5,$6,$7,$8) 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 ($5,$6,$7,$8) 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 ($7,$8) 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 ($2,$3,$4,$5,$6,$7,$8) IS NULL THEN
     'WHERE  cep.place_id = p.id' END
 , CASE WHEN eventtype           IS NOT NULL THEN 'AND e.type = $2'      END
 , CASE WHEN eventdate           IS NOT NULL THEN 'AND e.date = $3'      END
 , CASE WHEN eventcentury        IS NOT NULL THEN 'AND e.century = $4'   END
 , CASE WHEN constructiondate    IS NOT NULL THEN 'AND con.date = $5'    END
 , CASE WHEN constructioncentury IS NOT NULL THEN 'AND con.century = $6' END
 , CASE WHEN arstyle             IS NOT NULL THEN 'AND astyle.id = $7'   END
 , CASE WHEN artype              IS NOT NULL THEN 'AND atype.id = $8'    END
 , CASE WHEN NOT ($2,$3,$4,$5,$6,$7,$8) IS NULL THEN
     ')' END
   );
   USING  placename
        , eventtype
        , eventdate
        , eventcentury
        , constructiondate
        , constructioncentury
        , arstyle
        , artype;
END
$func$  LANGUAGE plpgsql;

这是一个完全重写,有几处改进。应使功能显着。也是 SQLi 安全的(就像你原来的一样)。在功能上应该是相同的除了我加入较少表的情况,这可能不会过滤通过单独加入表而过滤的行。

主要特点:

使用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?

【讨论】:

感谢您的回答和一般 cmets。我将重音/非重音字符放在非重音规则中,它可以工作。我认为不需要在 LIKE 中转义字符。至于您的其他评论,请查看我编辑的原始问题。 哦!对于lower(unaccent('%' || $1 || '%')) 部分,我不断收到ERROR: operator is not unique: unknown % unknown LINE 1: ...OWER (unaccent(place.name)) LIKE LOWER (unaccent('%' || $1 |... 之类的语法错误,这就是我使用placewithkeys := '%'||placename||'%'; 解决方案的原因 @slevin:您不应该遇到语法错误。我猜你的单引号是混乱的。这就是为什么我建议简化美元报价。在此过程中,我又改进了一些:'%' || lower(unaccent($1)) || '%'lower(unaccent('%' || $1 || '%')) 略好。 @slevin:考虑一下我添加的审核功能。 再次嗨,我花了一段时间检查发生了什么并在谷歌上搜索了一些细节,但我终于得到了你在重写中所做的事情——心态帮助我语法化了其他也用于搜索和组合不同的标准。你的逻辑很棒,比我的解决方案更有条理,处理各种情况也更巧妙。使用concat_ws 的不错选择。总的来说,我也认为这个版本是安全的,因为有USING。非常感谢。感谢您的努力和分享知识的能力。

以上是关于unaccent() 不适用于 plpgsql 动态查询中的希腊字母的主要内容,如果未能解决你的问题,请参考以下文章

CSS3 过渡不适用于显示属性 [重复]

sql 用于将ftp日志存储到PostgreSQL中的表的SQL和PLPGSQL代码(来自rsyslog)

plpgsql jsonb_set 用于带有嵌套数组的 JSON 对象数组

Flutter RTL(本地化)不适用于网格视图小部件 - 阿拉伯语言

如何在执行 SELECT... 语句时将表列传递给 plpgsql 函数

对于表中的行,将行保存在临时表中以在 plpgsql 的选择查询中使用其数据