Назад к блогу

PostgreSQL 20 удаляет array_nulls: как восстановление дампа превращает NULL в строку

PostgreSQL 20 удаляет array_nulls: как восстановление дампа превращает NULL в строку

PostgreSQL 20 полностью удаляет параметр array_nulls, из-за чего дампы, восстановленные в базы с нестандартными настройками, могут тихо превращать NULL-элементы массивов в строку «NULL». Разбираем механику этой ошибки на уровне array_in/array_out и COPY, историю параметра и причины, по которым его убрали именно сейчас.

В PostgreSQL 20 удалён параметр конфигурации array_nulls. Он управлял тем, как читается слово NULL внутри текстового представления массива, и существовал с 2005 года ради обратной совместимости. Вместе с ним закрылась лазейка, из-за которой восстановление дампа в базу с выключенным array_nulls тихо превращало NULL-элементы массивов в четырёхсимвольную строку NULL.

Что именно изменилось

array_nulls не «нейтрализован» — он удалён полностью. Удалённый параметр не принимает ни одного значения: SET array_nulls = on завершается ошибкой unrecognized configuration parameter "array_nulls", то же происходит при передаче его в options или PGOPTIONS. Строка array_nulls = on в postgresql.conf не даёт серверу запуститься, а при перезагрузке отклоняются все изменения в файле. Значение не имеет значения: on ломает все три случая так же, как off.

array_nulls входил в категорию «Previous PostgreSQL Versions» — набор параметров, существующих чисто для обратной совместимости. Категория существует с 7.4; за двадцать три года в ней побывало тринадцать параметров, шесть из них удалено: add_missing_from и regex_flavor в 9.0, sql_inheritance в 10, operator_precedence_warning в 14, escape_string_warning в 19 и array_nulls в 20.

Как работает механизм порчи

array_out обходит элементы и для каждого получает значение и признак «пусто». Если элемент пуст, в буфер пишется строка NULL, и кавычки для неё не ставятся. Если значение непустое, но пустая строка или совпадает со словом NULL без учёта регистра — кавычки ставятся принудительно, чтобы при чтении отличить его от настоящего NULL. При сборке результата элемент с флагом «нужны кавычки» печатается в двойных кавычках с экранированием кавычек и обратных слэшей, остальные — как есть.

Проблема на обратном пути. При разборе текстового массива в array_in элемент без кавычек и без экранирования, равный NULL без учёта регистра, распознаётся как NULL-элемент. Если же элемент был в кавычках или содержал экранирование, он считается обычным. То есть строка 'NULL', записанная без кавычек, при чтении станет NULL-элементом, а не строкой.

В COPY разбор устроен иначе: NULL-поле определяется сравнением с null_print — то есть с тем самым словом, которое в текстовом формате COPY помечает отсутствующее значение поля, — а не со словом NULL. Но pg_dump пишет null-элемент массива как неэкранированное слово NULL, и array_nulls никогда не входил в список настроек, которые дамп закрепляет при открытии, чтобы содержимое дампа читалось так же, как оно было записано. Восстановление в базу, где параметр выключен, читает эти NULL как текст.

Предыстория

Параметр добавил Том Лейн коммитом 17 ноября 2005 года. Он управлял тем, как читаются элементы массивов: при выключенном значении слово NULL в тексте массива воспринималось как обычная строка, а не как отсутствующее значение. Это было нужно для совместимости с поведением до версии 8.2, когда pg_dump записывал пустой элемент массива как незакавыченное слово NULL.

В документации параметр оказался в разделе Version and Platform Compatibility, который делится на подразделы Previous PostgreSQL Versions и Platform and Client Compatibility. Там собраны настройки, существующие ради совместимости со старыми версиями PostgreSQL или с поведением сторонних клиентов: например, lo_compat_privileges отключает новые проверки привилегий для совместимости с прошлыми выпусками, synchronize_seqscans при значении off возвращает поведение до 8.3, а transform_null_equals нужен для запросов, которые генерирует Microsoft Access. Исходное назначение таких параметров — не общая настройка сервера, а сохранение старого или клиент-специфичного поведения.

Почему удалили именно сейчас

За три дня до коммита пришёл BUG #19747. В отчёте показано, что pg_dump пишет пустой элемент массива как неэкранированное слово NULL, а array_nulls никогда не фиксировался в преамбуле дампа. При восстановлении в базу с выключенным array_nulls пустые элементы читаются как текст: без ошибки, с кодом возврата 0, тем же числом строк, и каждый null-элемент в каждой колонке text[] становится четырёхсимвольной строкой.

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

Авторы считают удаление безопаснее «нейтрализации»: нейтрализованный параметр всё ещё принимает своё единственное оставшееся значение, поэтому конфигурация, задающая значение по умолчанию, продолжает работать, тогда как удалённый не принимает ничего. Из этого следует поправка к правилу «оставить как есть»: нужно убедиться, что все остальные тоже её оставили.

Что это меняет на практике

Если база полагалась на array_nulls = off, при обновлении её ждут три разных сценария отказа. SET array_nulls = on падает с unrecognized configuration parameter "array_nulls", как и передача через options или PGOPTIONS. Строка в postgresql.conf не даёт серверу запуститься, а при перезагрузке отклоняются все изменения в файле. Сохранённая настройка в ALTER DATABASE ... SET, ALTER ROLE ... SET или на уровне функции заставляет pg_upgrade упасть на середине, хотя --check сообщил о совместимости.

Отдельно — тихая порча при восстановлении. Воспроизводимый сценарий: в prod есть таблица t (id int, v text[]) со строкой ARRAY['x', NULL, 'y'], в staging выполнено ALTER DATABASE staging SET array_nulls = off, затем pg_dump -d prod | psql -q -d staging. В prod запрос SELECT v, v[2] IS NULL FROM t возвращает {x,NULL,y} и t, в staging — {x,"NULL",y} и f. Число строк совпадает, ошибки нет, код возврата 0. Для колонки int[] хотя бы возникает ошибка, поскольку "NULL" не является целым числом, но без ON_ERROR_STOP восстановление продолжается и оставляет таблицу пустой.

Перед обновлением стоит проверить три места. По кластеру — соединение pg_db_role_setting с pg_database и pg_roles, разворот массива setconfig через unnest и фильтр по array_nulls, escape_string_warning, standard_conforming_strings. Поле setconfig хранит настройки уровня базы и роли, а разворот через unnest превращает этот массив в отдельные строки, чтобы фильтровать по именам параметров. По каждой базе — разворот proconfig из pg_proc, то есть настройки, заданные через SET внутри функций: proconfig — это поле функции, где лежат такие настройки, а разворот даёт по строке на каждую из них. По конфигурационным файлам — pg_file_settings с тем же фильтром. При этом standard_conforming_strings опасен только значением off, а array_nulls ломает pg_upgrade при любом значении. Эти запросы не видят -c в командной строке postmaster, options в строке подключения клиента и SET из кода приложения или тела функции — для них нужен grep.

Ограничения и открытые вопросы

Что делать с уже повреждёнными дампами и как обнаружить порчу постфактум, не описано. Предложенное в отчёте исправление начиналось с добавления ещё одного SET в преамбулу дампа, но Том Лейн ответил, что параметр, который устанавливает pg_dump, уже никто никогда не сможет удалить. В итоге параметр удалён в master (будущий PostgreSQL 20); в бэк-ветках ничего не менялось. PostgreSQL 19, находившийся в бете на момент коммита, всё ещё содержит array_nulls — первый выпуск без него будет PostgreSQL 20.

Источники

Похожее