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/9914900Postgres 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
子句引用表place
,FROM
子句无论如何都需要包含此表。因此,您不能从一开始就独立连接 FROM 子句。最好将它们全部保存在一个 format()
中。
使用美元引用,这样您就不必额外转义单引号。
Insert text with single quotes in PostgreSQL What are '$$' used for in PL/pgSQL也许只需使用ILIKE
而不是lower(...) LIKE lower(...)
。如果您使用三元组索引(对于这个查询来说似乎是最好的):那些也适用于ILIKE
:
我假设您知道您可能需要转义 LIKE
模式中具有特殊含义的字符?
审核功能
在你提供完整的功能之后...
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
。这为更好的性能贡献了最大的份额。相关:
format()
通常是从用户输入连接 SQL 的好选择。但是由于您封装了所有代码元素并且只传递标志,因此您不需要它在这种情况下。相反,concat_ws()
是有帮助的。相关:
仅连接您实际需要的 JOIN。
更少的分配,更短的代码。
参数的默认值。允许缺少参数的简化调用。喜欢:
SELECT __a_search_place('foo', 2, 3, 4);
SELECT __a_search_place('foo');
相关:
Optional argument in PL/pgSQL function关于用于测试任何值是否为NOT NULL
的简短ROW()
语法:
【讨论】:
感谢您的回答和一般 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 动态查询中的希腊字母的主要内容,如果未能解决你的问题,请参考以下文章
sql 用于将ftp日志存储到PostgreSQL中的表的SQL和PLPGSQL代码(来自rsyslog)
plpgsql jsonb_set 用于带有嵌套数组的 JSON 对象数组
Flutter RTL(本地化)不适用于网格视图小部件 - 阿拉伯语言