
ora2pg переносит ~80% Oracle-схемы. А что происходит с оставшимися 20%?
Если вы хоть раз мигрировали с Oracle на PostgreSQL (или на Postgres Pro Standard/Certified, без лицензии на Enterprise и без проприетарной ora2pgpro), вы наверняка уже знакомы с ora2pg. Это открытый и по-настоящему рабочий конвертер: по независимым оценкам он закрывает в среднем около 80% работы по переносу PL/SQL в PL/pgSQL. Для инструмента, который несколько человек делают в свободное время против коммерческой СУБД с тридцатилетней историей, это очень много.
Проблема не в этих 80%, а в оставшихся 20%. Точнее, в том, как именно они себя ведут.
Молчание вместо ошибки
Когда конвертер не справляется с чем-то синтаксически сложным, он обычно об этом говорит: падает, ругается, пишет ERROR. Неприятно, но честно: о проблеме узнаёшь сразу, в момент конвертации.
ora2pg в самых интересных случаях ведёт себя иначе. Он либо тихо выбрасывает конструкцию, которую не умеет переносить, либо переносит её с багом, который никак себя не проявляет до первого реального вызова в проде. CREATE TABLE при этом отрабатывает без единой ошибки, схема разворачивается, тесты «схема развернулась» зелёные. А дальше как повезёт.
Ниже — пара примеров, каждый прогнан через настоящий ora2pg 25.0 и настоящий PostgreSQL 16, а не пересказан по документации.
READ ONLY таблица. В Oracle это гарантия на уровне сервера: любой INSERT/UPDATE/DELETE в такую таблицу падает с ORA-12081, кто бы ни пытался, хоть владелец схемы.
CREATE TABLE audit_log (
log_id NUMBER,
message VARCHAR2(200)
) READ ONLY;
ora2pg конвертирует это так:
CREATE TABLE audit_log (
log_id bigint,
message varchar(200)
) ;
Секция READ ONLY просто исчезла, никакого предупреждения. Проверяем на реальном PostgreSQL:
INSERT INTO audit_log VALUES (1, 'should have been blocked in Oracle');
-- INSERT 0 1
Прошло. В Oracle этот же INSERT был бы гарантированно заблокирован. Если READ ONLY был единственной защитой таблицы-снапшота или архива от случайной записи, после миграции этой защиты просто нет. И никто об этом не узнает, пока кто-нибудь случайно (или не случайно) не запишет туда что-то лишнее.
Баг с двойными скобками в IDENTITY. Тут уже не пропуск конвертации, а самый настоящий баг в подстановке ora2pg.
CREATE TABLE customers (
customer_id NUMBER GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1),
name VARCHAR2(100)
);
Конвертируется в:
CREATE TABLE customers (
customer_id bigint GENERATED ALWAYS AS IDENTITY ((START WITH 1 INCREMENT BY 1)),
name varchar(100)
) ;
Смотрите на ((START WITH.... Лишняя пара скобок, и CREATE TABLE падает уже на загрузке DDL:
ERROR: syntax error at or near "("
Это самая ранняя по времени проявления находка из всего реестра, даже раньше первого вызова функции. И особенно неприятная, потому что GENERATED ... AS IDENTITY — современный и всё более распространённый способ объявлять auto-increment колонку в Oracle 12c+. Отдельная проверка: без опций в скобках (просто GENERATED ALWAYS AS IDENTITY, без START WITH/INCREMENT BY) конвертируется нормально. Баг именно в обработке опций.
CROSS APPLY. Тут ловушка тоньше. Пакет с процедурой компилируется без единой ошибки, потому что ora2pg просто копирует CROSS APPLY(...) как есть. А PostgreSQL про APPLY вообще ничего не знает, и падает не при деплое, а при первом вызове:
ERROR: syntax error at or near "APPLY"
Со стороны это выглядит так, будто код развернулся и всё в порядке. А «не в порядке» вылезает только когда кто-то реально дёрнет эту процедуру.
Вот как эти три находки выглядят вместе в отчёте инструмента. Реальный вывод, не мокап:
Откуда уверенность, что это не выдумки
Здесь важна методология, потому что «звучит по-ораклиному специфично» — плохой критерий сам по себе. Часть гипотез, которые интуитивно казались проблемными, на практике не подтвердилась и в реестр не попала. Например, CREATE PACKAGE выглядел очевидным кандидатом на проблемы, но ora2pg переносит его без нареканий.
Правило простое: детектор появляется только после того, как гипотеза подтверждена на практике, а не просто выглядит правдоподобно.
- Берётся конкретная Oracle-конструкция.
- Собирается минимальный воспроизводимый пример.
- Пример прогоняется через настоящий
ora2pg. - Результат загружается в настоящий PostgreSQL — смотрим, что получилось на самом деле.
- Если справился — гипотеза отклоняется. Если нашёлся воспроизводимый баг — заводится тест-фикстура и пишется детектор.
Отдельно все детекторы прогонялись на ~140 тысячах строк реального открытого PL/SQL-кода (пакеты alexandria-plsql-utils и официальные демо-схемы Oracle db-sample-schemas), чтобы убедиться, что они не начинают ложно срабатывать на нормальном коде. Там, кстати, нашлись и настоящие живые попадания. Например, sup_text_idx из официальной Oracle-схемы SH реально использует INDEXTYPE IS CTXSYS.CONTEXT (Oracle Text), которого в PostgreSQL просто нет. Такие находки остаются в проекте постоянными регрессионными тестами: не гипотетический пример, а код, который правда существует.
Забавный момент про сам инструмент
Раз уж рассказ честный, вот ещё одна история. В какой-то момент код-ревью собственных изменений нашёл системный баг в уже выпущенных детекторах. DBMS_METADATA.GET_DDL, стандартный способ выгрузить DDL из Oracle-схемы, по умолчанию не ставит ; в конце оператора. А часть детекторов определяла границу «своего» оператора так: до следующей ;, а если её нет, до конца файла. На несклеенном экспорте это означало, что конструкция из второй таблицы могла по ошибке приписаться первой, если первая шла без точки с запятой.
Починил общим хелпером: он ограничивает оператор либо ближайшей ;, либо началом следующего оператора того же типа, смотря что наступит раньше. Урок простой: даже инструмент, который сам ищет чужие баги, нуждается в таком же скепсисе к себе, как и всё остальное.
Что в итоге
Получился небольшой сканер, не замена ora2pg, а надстройка над ним, которая запускается до миграции. Он смотрит на схему Oracle и для каждой проблемной конструкции показывает: что именно с ней случится, почему, и на что заменить руками. На сегодня в реестре 28 подтверждённых находок, у каждой есть воспроизводимый пример, реальный вывод ora2pg и тесты, включая guard-тесты на ложные срабатывания.
Сама библиотека детекторов — чистый Python без единой внешней зависимости, можно дёргать из своих скриптов вообще без установки чего-либо ещё. У CLI-обёртки одна зависимость, rich, ради приличного терминального вывода.
pip install ora2pg-gap-report
ora2pg-gap-report path/to/schema_dump.pkb another_file.sql
Код и вся доказательная база лежат на GitHub: Lunch418/ora2pg-gap-report, MIT. Если у вас есть своя Oracle-схема с чем-то диким, чего нет в реестре, заводите issue. Мне правда интересно найти это и разобрать так же честно, как всё остальное здесь.