В предыдущих постах вы могли прочитать про INCLUDE-индексы и встроенный веб-поиск. Эти фичи появятся в PostgreSQL 11, релиз которого, напомню, планируется в октябре. Сегодня мне хотелось бы продолжить обзор новых фичей грядущего релиза, рассказав про JSONB-трансформы для языков PL/Python (3f44e3db) и PL/Perl (341e1661). Оба патча написаны Антоном Быковым.

Как вам может быть известно, PostgreSQL позволяет писать хранимые процедуры на разных языках, в том числе на C, PL/pgSQL, PL/Perl, PL/Python и других. Также PostgreSQL позволяет хранить JSON, строить по нему индексы и производить различные манипуляции с документами. Для хранения документов есть два встроенных типа: JSON, являющийся по сути обычной строкой, и JSONB, реализующий более эффективное бинарное представление.

Однако на границе этих механизмов есть небольшой косяк. Проще всего объяснить его на примере:

-- Важно! Синтаксис `create language` задепрекейчен с 9.1
create extension plpython3u;

CREATE OR REPLACE FUNCTION foo(val jsonb) RETURNS int
LANGUAGE plpython3u
AS $$
assert(isinstance(val, str))
return len(val)
$$;

select foo('{"aaa":123, "ййй":456}' :: jsonb);

В результате код выполнится без ошибок и вернет результат 24. То есть, при передаче в хранимую процедуру JSONB был в закодирован в строку, от которой мы вычислили длину.

Fun fact! На самом деле, длина исходного документа — 22 символа, а у нас почему-то получилось 24. Как ни странно, никакого бага здесь нет, как можно было бы предположить. Попробуйте в качестве домашнего задания выяснить, чем вызвано такое несоответствие.

Так вот, но ведь мы хотели передать документ, а не строку. Само собой разумеется, строку можно декодировать обратно:

CREATE OR REPLACE FUNCTION bar(val_str jsonb) RETURNS int
LANGUAGE plpython3u
AS $$
import json
val = json.loads(val_str)
assert(isinstance(val, dict))
plpy.info(sorted(val.items()))
return len(val)
$$;

select bar('{"aaa": 123, "ййй": 456}' :: jsonb);

Результат:

INFO: [('aaa', 123), ('ййй', 456)]
bar
-----
2
(1 row)


Вроде, похоже на правду. Но если подумать, мы тут делаем много бесполезной работы — сначала кодируем документ, а затем тут же декодируем. Если документ большой, а хранимая процедура вызывается часто, накладные расходы на это кодирование-декодирование могут быть довольно большими.

Спрашивается, а нельзя ли просто передать JSONB в Python или Perl в виде понятных им объектов (массивов, словарей/хэшей, и так далее)? Оказывается, что можно, и названные патчи делают именно это. Вот как этим примерно пользоваться:

create extension jsonb_plpython3u;

CREATE OR REPLACE FUNCTION baz(val jsonb) RETURNS int
LANGUAGE plpython3u
TRANSFORM FOR TYPE jsonb
AS $$
assert isinstance(val, dict)
plpy.info(sorted(val.items()))
return len(val)
$$;

Кстати, в обратную сторону, то есть, кодировать возвращаемые документы, тоже можно:

CREATE OR REPLACE FUNCTION qux(val int) RETURNS jsonb
LANGUAGE plpython3u
TRANSFORM FOR TYPE jsonb
AS $$
obj = { "val": val }
return obj
$$;

Для Perl и Python 2 все делается аналогично, поэтому соответствующий код я не привожу. Кроме того, не стану грузить вас тонкостями работы кода в различных граничных случаях, например, что происходит, если в возвращаемом документе встретятся комплесные числа, NaN, ±Inf, и вот это вот все. Заинтересованных читателей я призываю ознакомиться с кодом патчей. Они обильно обмазаны тестами, как раз проверяющими подобные граничные случаи.

Такая вот интересная фича. Конечно, ситуации, в которых она оказывается полезна, возникают не часто. Но знать про существование JSONB-трансформов, все же, не повредит.

Комментарии (2)


  1. SlavniyTeo
    25.04.2018 16:27

    {"aaa":123, "ййй":456}

    Попробуйте в качестве домашнего задания выяснить, чем вызвано такое несоответствие.

    При переводе из JSONB в TEXT после двоеточий добавляются пробелы. Может ведь ещё и поля местами поменять.


    1. afiskon Автор
      25.04.2018 17:09

      Верно :)