В этой статье речь пойдёт о методике разработки в Postgres, которую можно использовать для лучшей интеграции вашего расширения с базой данных. А конкретнее - постараюсь кратко и по существу рассказать о том, как реализовать кастомные свойства объектов БД (a.k.a. custom reloptions), которые можно было бы назначать разным видам объектов БД и таким образом управлять поведением расширения.
Зачем это нужно? Например, для меня как разработчика разного рода костылей и подпорок для оптимизатора Postgres есть прямой интерес в том, чтобы дать пользователю возможность указывать приоритет (а возможно даже и понижающий cost) для индексов - это известная проблема, когда при прочих равных Postgres выбирает первый попавшийся индекс. Поэтому было бы неплохо иметь возможность при помощи расширения добавить команду наподобие:
ALTER TABLE … SET index_weight = ‘..’.
Вероятно, такой степени интеграции с SQL диалектом Postgres добиться сложно, да и едва ли нужно в интересах переносимости. И все же хочется получить возможность задавать параметр при помощи вызова вида:
SELECT my_extension.set_index_weight('index_name', 'value')
таким образом указывая расширению корректировать стоимость планов запросов, использующих даный индекс, и влиять на финальный выбор оптимизатора PostgreSQL.
Предложения о кастомизации свойств некоторых объектов уже несколько раз обсуждались в сообществе (например, см. здесь и здесь), и даже предлагались патчи ядра. Однако их код достаточно объёмен, а область применения узка (или пока это так только выглядит), а это ставит практичность их внедрения в ядро под вопрос. Более того, иногда хочется привязать свойство не к таблице, а, например, к индексу, large object или типу данных - и это вопрос времени, усложнения кода и того факта, что принятый патч изменит поведение только будущих версий ядра СУБД. Однако что делать, если такая функциональность нужна сегодня?
Чтобы понять, как это сделать, определим сперва требования к функциональности:
Свойство должно иметь внутреннюю связь с объектом БД - удаляться вместе с ним по команде DROP .. CASCADE, например.
Свойство должно иметь некоторую ассоциацию с нашим расширением, чтобы СУБД могла корректно обработать ситуацию, когда расширение уже удалено из системы.
Свойство должно вести себя транзакционно и следовать правилам видимости. То есть, будучи изменено одновременно в параллельно выполняющихся транзакциях, новое значение свойства должно быть видимо только внутри транзакции до момента коммита и возвращаться к исходному состоянию на ROLLBACK.
upgrade, dump/restore должны корректно переносить это свойство вместе с базой.
Конечно, хотелось бы не только транзакционного, но и сессионного поведения, наподобие GUC’ов Postgres - но, полагаю, реализовать это будет намного труднее.
Таким образом, использование простой глобальной хэш-таблицы, в которой ключом выступал бы Oid объекта, нам не подходит, поскольку и значение параметра может быть переменной длины (например, строка), и реализовать связь объекта со свойством достаточно сложно. Но главное - как обеспечить транзакционное поведение и MVCC?
Единственный простой способ удовлетворить пункт 3 - таблица в БД. Может быть, стоит создать таблицу вида <ключ><значение> как часть расширения и добавлять свойство в неё, а расширение будет извлекать из этой таблицы его значение? Вариант! Однако имея обширный опыт использования таблиц в расширениях (см, например, AQO и sr_plan), я могу сказать, что такая история очень сомнительна ввиду того, что вызывает проблемы разного свойства. И это не только задача апгрейда, dump/restore и репликации, но и постоянной необходимости проверять, что объект существует в базе. А ещё есть оверхед на поиск…
Моя конкретная задача состояла в создании свойства 'delta_apply' для колонки произвольной таблицы. Это свойство предназначено для управления логической репликацией и должно определять механизм разрешения конфликта вида UPDATE/UPDATE для числовых значений. Если это свойство установлено, то subscriber вместо использования различных стратегий разрешения конфликта вычисляет дельту между новым и старым значением столбца и добавляет его к текущему на subscriber'e.
Для такого параметра актуальны все вышеприведенные требования. А для того, чтобы удовлетворить эти требования в PostgreSQL, нашёлся только один механизм - SECURITY LABELS.
Как это работает? Посмотрим на схему:

Решение базируется на использовании системной таблицы pg_seclabel, записи которой by-design привязываются к объектам БД. Добавление, обновление и удаление записи в этой таблице происходит при помощи UTILITY-команды 'SECURITY LABEL ..', что даёт возможность мониторить этот процесс в расширении при помощи utility hook'a.
Инструмент SECURITY LABEL позволяет маркировать объекты многих типов, включая VIEW, и даже функции, и этого должно быть достаточно для большинства задач. Запись о метке содержит указание на Oid объекта и класс объекта - таблица, атрибут, функция и т.д. - вполне удобно. Текстовое поле метки позволяет поместить любой набор данных, которые мы хотим привязать к данному объекту, а поле 'provider' позволяет отличать метки, созданные конкретным модулем.
Для минимизации оверхеда (ходить каждый раз в таблицу дорого, а системный кэш к таблице pg_seclabel пока не добавили) добавляем кэш в виде локальной хэш-таблицы. Невалидные записи в этой таблице удаляются за счёт регистрации RelcacheCallback’a, задача которого - инвалидировать запись при любом вызове ALTER TABLE на наш объект.
Логику добавления записей в кэш можно реализовывать по-разному в зависимости от вариантов предполагаемого использования. Для моей задачи достаточно добавлять запись в этот кэш в момент обращения к объекту в хуке (если вызов приходит из ядра), либо же в UI-функции расширения (если действие инициировано пользователем).
Тонкости реализации добавления и удаления свойства объекта
Для того, чтобы локальные кэши наших свойств сохраняли консистентность, нужно при каждом изменении релевантных объектов отправлять, а в каждом бэкенде - принимать сообщение инвалидации объекта и затирать соответствующую запись в кэше. Если изменение происходит с самим объектом, то, как было сказано ранее, мы обрабатываем это событие в
RelcacheCallback'е и помечаем запись как невалидную. Однако что делать, если мы изменяем само свойство - а это по факту просто DML в таблицеpg_seclabel. Как вовремя сообщить об этом всем остальным процессам?В Postgres есть инструмент для отправки invalidation message -
CacheInvalidateRelcacheByRelid. Он удобен, однако ограничен объектами из pg_class. Если свойство назначается например на тип данных, данная функция не поможет. Поэтому на практике в момент добавления или удаления security label я инициирую обновление самого объекта, по факту не меняя в нём ничего, а соответствующий invalidation callback делает пометку в локальном кэше расширения.
Расширение предоставляет пользователю интерфейс в виде функции set_property(), которая назначает SECURITY LABEL на указанный объект. В описании метки указывается значение свойства объекта, например: 'delta_apply: true'. Callback seclabel_provider, имплементируемый в расширении при каждом создании метки, контролирует корректность объекта и задаваемого свойства.
Таким образом, у клиента и расширения появляется новый инструмент коммуникации, прми этом достаточно нативный, хотя и реализованный вне ядра. Расширение теперь всегда имеет возможность проверять, не назначил ли пользователь какое-то свойство, и соответствующим образом менять поведение.
В коде delta_apply фичи это используется следующим образом (см. для примера коммиты 67bf955, 09d5d7e). Процесс-subscriber логической репликации получает UPDATE-запись, содержащую старое и новое значение строки таблицы. Открывая таблицу, мы одновременно обращаемся к кэшу delta_apply свойства. Если таблица включает в себя один или более таких 'инкрементальных' атрибутов, для каждого из них вычисляется дельта между старым и новым пришедшими значениями и прибавляется к тому значению, что находится в базе subscriber'a. Даже если в соответствии со стратегией разрешения конфликта (например, last-update-wins) будет принято решение отвергнуть входящее обновление, изменения delta_apply колонок тем не менее будут учтены. Таким образом уменьшается вероятность появления конфликта между записями и появляется гарантия, что все обновления инкрементальной колонки будут учтены.
P.S. В ходе подготовки публикации выяснилось, что механизм security labels уже используется похожим образом в проекте AWS pgactive (и, видимо, его предшественника BDR). Изучение реализованного там варианта использования данного механизма можно начать, например, отсюда.
THE END.
Испания, Мадрид, 14 декабря 2025 года.