为了放弃阅读整个问题,我的基本问题是: 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 || ' (%)'
此检查不会验证所讨论的%
是否为数字,并且它不会考虑多个括号,如“Name((1))”,如果存在任何一种情况,则会抛出强制转换异常。
WHERE
声明确实需要更像:
WHERE a.r1_name ~* var_name || E' \\(\\d+\\)'
但var_name
可以包含正则表达式字符,这导致上面的问题:PostgreSQL中是否有一个函数可以转义字符串中的正则表达式字符,所以我可以这样做:
WHERE a.r1_name ~* regex_escape(var_name) || E' \\(\\d+\\)'
非常感谢任何建议,包括可能重复我的重复名称解决方案。
如何尝试这样的事情,用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
方法将不是一个好方法。
要在顶部解决问题:
让我们从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
图案的吊坠,其中只有三个字符是特殊的:
\%_
默认转义字符是反斜杠,但可以使用
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
您是否可以自由更改架构?我认为如果您可以使用复合主键,问题就会消失:
name text not null,
number integer not null,
primary key (name, number)
然后它成为显示层的职责,将Fred#0显示为“Fred”,将Fred#1显示为“Fred(1)”,&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)