Функция Escape для регулярных выражений или шаблонов LIKE - PullRequest
6 голосов
/ 28 февраля 2011

Чтобы отказаться от чтения всей проблемы, мой основной вопрос:
Есть ли в 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+\\)'

Любые предложения приветствуются, включая возможную переработку моего решения для дублирующих имен.

Ответы [ 3 ]

7 голосов
/ 17 августа 2017

Чтобы ответить на вопрос сверху:

Функция выхода из регулярного выражения

Давайте начнем с полного списка символов со специальным значением в регулярное выражение паттерны:

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

Завернуты в скобочное выражение Большинство из них теряют свое особое значение - за некоторыми исключениями:

  • -должен быть первым или последним, или это означает диапазон символов.
  • ] и \ должны быть экранированы с \.

После добавления захвата скобок для обратной ссылки ) ниже мы получаем этот шаблон регулярного выражения:

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

Используя его, эта функция экранирует все специальные символы с обратной косой чертой (\) -тем самым удаляя специальное значение:

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 escape-функция

Для полноты, подвеска для 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
1 голос
/ 28 февраля 2011

как насчет того, чтобы сделать что-то подобное, заменив 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 не будет хорошим.

0 голосов
/ 28 февраля 2011

Можете ли вы изменить схему?Я думаю, что проблема исчезла бы, если бы вы могли использовать составной первичный ключ:

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)
...