用于正则表达式或LIKE模式的转义函数

时间:2011-02-28 15:36:59

标签: regex postgresql escaping pattern-matching plpgsql

放弃阅读整个问题,我的基本问题是:
PostgreSQL中是否有一个函数来转义字符串中的正则表达式字符?

我已经探测过文档,但无法找到这样的功能。

以下是完整的问题:

在PostgreSQL数据库中,我有一个包含唯一名称的列。我还有一个定期在此字段中插入名称的进程,并且为了防止重复,如果需要输入已存在的名称,它会在末尾附加一个空格和括号,并附加一个计数。

即。名称,名称(1),名称(2),名称(3)等

目前,我使用以下代码查找要在系列中添加的下一个数字(用plpgsql编写):

var_name_id := 1;

SELECT CAST(substring(a.name from E'\\((\\d+)\\)$') AS int)
INTO var_last_name_id
FROM my_table.names a
WHERE a.name LIKE var_name || ' (%)'
ORDER BY CAST(substring(a.name from E'\\((\\d+)\\)$') AS int) DESC
LIMIT 1;

IF var_last_name_id IS NOT NULL THEN
    var_name_id = var_last_name_id + 1;
END IF;

var_new_name := var_name || ' (' || var_name_id || ')';

var_name包含我要插入的名称。)

现在可以使用,但问题在于WHERE声明:

WHERE a.name LIKE var_name || ' (%)'

此检查不会验证相关的%是否为数字,并且不会考虑多个括号,例如“名称((1))”,如果存在任何一种情况将抛出一个强制转换异常。

WHERE语句确实需要更像:

WHERE a.r1_name ~* var_name || E' \\(\\d+\\)'

但是var_name可以包含正则表达式字符,这导致了上面的问题:PostgreSQL中是否有一个函数可以转义字符串中的正则表达式字符,所以我可以这样做:

WHERE a.r1_name ~* regex_escape(var_name) || E' \\(\\d+\\)'

非常感谢任何建议,包括可能重复我的重复名称解决方案。

3 个答案:

答案 0 :(得分:7)

要在顶部解决问题:

正则表达式转义函数

让我们从regular expression模式中具有特殊含义的完整字符列表开始:

!$()*+.:<=>?[\]^{|}-

包裹在bracket expression中的大部分都失去了特殊意义 - 除了少数例外:

  • -必须是第一个或最后一个,否则它代表范围字符。
  • ]\必须使用\进行转义。

在下面添加capturing parentheses for the back reference)后,我们得到了这个正则表达式模式:

([!$()*+.:<=>?[\\\]^{|}-])

使用它,此函数使用反斜杠(\)转义所有特殊字符 - 从而删除特殊含义:

CREATE OR REPLACE FUNCTION f_regexp_escape(text)
  RETURNS text AS
$func$
SELECT regexp_replace($1, '([!$()*+.:<=>?[\\\]^{|}-])', '\\\1', 'g')
$func$  LANGUAGE sql IMMUTABLE;

演示

SELECT f_regexp_escape('test(1) > Foo*');

返回:

test\(1\) \> Foo\*

同时:

SELECT 'test(1) > Foo*' ~ 'test(1) > Foo*';

返回FALSE,这可能会让天真的用户感到惊讶,

SELECT 'test(1) > Foo*' ~ f_regexp_escape('test(1) > Foo*')

现在应该返回TRUE

LIKE转义函数

为了完整性,LIKE图案的吊坠,其中只有三个字符是特殊的:

\%_

The manual:

  

默认转义字符是反斜杠,但可以使用ESCAPE子句选择不同的转义字符。

此函数采用默认值:

CREATE OR REPLACE FUNCTION f_like_escape(text)
  RETURNS text AS
$func$
SELECT replace(replace(replace($1
         , '\', '\\')  -- must come 1st
         , '%', '\%')
         , '_', '\_');
$func$  LANGUAGE sql IMMUTABLE;

我们也可以在这里使用更优雅的regexp_replace(),但对于只有少数几个字符,replace()函数的级联速度更快。

演示

SELECT f_like_escape('20% \ 50% low_prices');

返回:

20\% \\ 50\% low\_prices

答案 1 :(得分:1)

如何尝试这样的事情,用var_name代替我的硬编码'John Bernard'

create table my_table(name text primary key);
insert into my_table(name) values ('John Bernard'), 
                                  ('John Bernard (1)'), 
                                  ('John Bernard (2)'), 
                                  ('John Bernard (3)');


select max(regexp_replace(substring(name, 13), ' |\(|\)', '', 'g')::integer+1) 
from my_table 
where substring(name, 1, 12)='John Bernard' 
      and substring(name, 13)~'^ \([1-9][0-9]*\)$';

 max
-----
   4
(1 row)

一个警告:我假设在此过程运行时单用户访问数据库(您的方法也是如此)。如果情况并非如此,则max(n)+1方法不会很好。

答案 2 :(得分:0)

您是否可以自由更改架构?我认为如果您可以使用复合主键,问题就会消失:

name text not null,
number integer not null,
primary key (name, number)

然后显示层的责任是将Fred#0显示为“Fred”,Fred#1显示为“Fred(1)”,&amp; c。

如果您愿意,可以为此职责创建视图。这是数据:

=> select * from foo;
  name  | number 
--------+--------
 Fred   |      0
 Fred   |      1
 Barney |      0
 Betty  |      0
 Betty  |      1
 Betty  |      2
(6 rows)

观点:

create or replace view foo_view as
select *,
case
  when number = 0 then
    name
  else
    name || ' (' || number || ')'
end as name_and_number
from foo;

结果:

=> select * from foo_view;
  name  | number | name_and_number 
--------+--------+-----------------
 Fred   |      0 | Fred
 Fred   |      1 | Fred (1)
 Barney |      0 | Barney
 Betty  |      0 | Betty
 Betty  |      1 | Betty (1)
 Betty  |      2 | Betty (2)
(6 rows)