Миграция 40-летней Clipper ERP: orphan-строки как способ выжить

Joseph Sprei мигрировал 40-летнюю Clipper ERP и нашёл 41 095 orphan-строк на $872 000. Три фазы расследования: сначала виноватым казался pipeline, потом старый вендор, в финале — оказалось, что система намеренно так устроена и именно так прожила 40 лет.

Обложка: Миграция 40-летней Clipper ERP: orphan-строки как способ выжить

Если вам предстоит вытащить данные из старой системы и в новой схеме появятся orphan-строки (записи в дочерней таблице без родителя), не торопитесь чинить — возможно, это и есть нормальное состояние, к которому система пришла за десятилетия работы. Ровно с такой ситуацией столкнулся Joseph Sprei, мигрируя 40-летнюю Clipper ERP (Clipper — диалект xBase для DOS-приложений 80-90-х, использует .PRG-файлы исходников и .DBF-файлы данных) на Delphi и PostgreSQL. После первого прохода — 41 095 осиротевших строк деталей счетов и 4 160 платежей, у которых не было соответствующего заголовка, на общую сумму $872 609. Три фазы расследования показали: это не баг миграции и не косяк старого вендора, а сознательное поведение ERP, благодаря которому система прожила 40 лет на железе из 90-х.

Joseph Sprei, основатель Ask the Ledger (on-premise ERP для wholesale-дистрибьюторов), выложил подробный разбор кейса в блоге компании. Это редкая статья, где legacy-миграция раскрывается с архитектурной, а не «вот сколько строк мы переписали»-стороны: что в старом коде казалось багом, и почему это в реальности было механизмом выживания.

Главное
Ключевые выводы
Что нашёл Sprei, мигрируя 40-летнюю Clipper ERP
  • Clipper ERP — 570 .PRG-файлов, четыре десятилетия .DBF-файлов, миллионы строк. После миграции на Delphi и PostgreSQL — 41 095 orphan-строк деталей счетов (1,36%) и 4 160 orphan-платежей (1,19%), $872 тысячи.
  • Расследование прошло три фазы: «это мы виноваты», «это старый вендор», и финал — «это и есть способ, которым система выжила 40 лет». Старый ERP периодически чистил заголовки счетов, но не каскадировал чистку в детали и платежи — и продолжал работать.
  • Из 570 .PRG-файлов реальный бизнес делают всего семь. Остальные — устаревшие отчёты, разовые утилиты, мёртвые эксперименты. Mapping использования по меню переломил план миграции куда сильнее, чем код-анализ.
  • AI ускорил «археологию» — сканирование 570 файлов, mapping программ к меню и DBF, выделение бизнес-правил, генерацию reconciliation-запросов. Но не заменил verification loop: каждый вывод подтверждался повторяемым SQL и cross-check архивов.
  • Финальный результат: PASS-with-known-exceptions. Orphan-данные приняли как permanent legacy condition с нулевым операционным влиянием на текущий бизнес. И перестали делать вид, что можно «починить» данные, которых больше нет.

Что такое orphan-строки и откуда взялись числа

Orphan-строка — это запись в дочерней таблице (детали счёта, платёж), у которой нет соответствующего родителя в headers-таблице. Самый банальный SQL-запрос для проверки в PostgreSQL:

			SELECT COUNT(*)
FROM invoice_lines il
WHERE NOT EXISTS (
    SELECT 1 FROM invoices i
    WHERE i.inv_no = il.inv_no
);
-- 41,095
		

В цифрах кейса Sprei это 41 095 orphan-строк деталей из 3 012 516 (1,36%) и 4 160 orphan-платежей из 350 957 (1,19%). В деньгах: $530 833,66 «осиротевших» сумм по строкам ($124,5 млн оборота — 0,42%) и $341 775,36 по платежам ($115,4 млн — 0,30%). В процентах звучит мелко. В абсолютных числах — это «нам надо как-то объяснить почти миллион долларов несведёнки».

Фаза 1: «мы виноваты»

Команда исходила из того, что это дефект миграции. Прошлись по всему пайплайну:

  • Trim и pad ключей (Clipper-индексы чувствительны к trailing-пробелам).
  • Порядок импорта — строки до заголовков? Заголовки до строк?
  • Маппинг файлов: не пропустили ли молча целую колонку?
  • Удалённые DBF-записи (Clipper помечает удаление флагом, не настоящим delete).
  • Перезапуск reconciliation после каждой правки.

К третьему дню в таблице записей были «мёртвые гипотезы», и ни одна не объясняла orphan-числа. Pipeline жил.

Фаза 2: «это старый вендор накосячил»

Когда собственный пайплайн прошёл проверку, естественный шаг — посмотреть на тех, кто построил систему. Clipper использует двузначный год в именах таблиц, а сами имена — telegraph-style сокращения: AR — accounts receivable, дебиторка; MAST — master (заголовки), TRAN — transactions (строки), CASH — платежи. ARMAST99 — текущие заголовки счетов, ARTRAN99 — строки, ARCASH99 — платежи. Команда ожидала найти десятилетние архивы — ARMAST70, ARMAST80, ARMAST88, ARMAST98 — и в них найти orphan-headers.

Архивы оказались практически пустыми: ARMAST70 и ARMAST80 не существовали вообще, в ARMAST88 — три заголовка, в ARMAST98 — один. После импорта всех найденных архивов orphan-числа сдвинулись на жалкие +4 заголовка, +24 строки, +10 платежей. 41 095 и 4 160 остались на местах.

В этот момент картина перевернулась: команда не «не нашла историю», история была сознательно отсечена. Это и было следующей стадией расследования.

Фаза 3: «это и есть способ выжить 40 лет»

Sprei смотрит на диапазон номеров счетов в активной ARMAST99. MIN = 298 417, MAX = 3 462 381. Orphan-строки ссылаются на номера счетов ниже 298 417 — без следа в архивах — и до 3 045 913. Получается, система периодически вычищала старые заголовки счетов из активной таблицы (видимо, чтобы держать .DBF-файл управляемого размера), но не каскадировала чистку в файлы строк и платежей. Транзакционные записи переживали свои заголовки.

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

Sprei формулирует переход в одной фразе: «Я виноват. Старый вендор виноват. Нет. Это то, как система прожила 40 лет».

Странности оказались не случайными

С позиции современного greenfield-проекта вся Clipper ERP выглядит как набор анти-паттернов:

  • VOIDED MM/DD/YY в operational-полях (например, в поле cust_no могла лежать пометка void).
  • Смешанные форматы дат в одной колонке (MM/DD/YY и YYYYMMDD одновременно — в зависимости от эпохи).
  • Referential integrity на уровне приложения, без foreign-key constraints в БД.
  • Денормализованные балансы, которые транзакционно поддерживаются в карточке клиента.
  • Periodic header purges — те самые, что и оставили orphan-строки.

Sprei делает важный поворот: с точки зрения долгожительства это не анти-паттерны, а адаптация. Каждый из них был обходом ограничения, которого в современных системах нет — лимиты на размер файла, single-user lock индекса, дорогие диски, отсутствие нормальных бэкапов. Ограничения исчезли, обходы остались — потому что система продолжала работать.

570 файлов, 7 значимых

Кодовая база выглядела как 570-файловая гора. Команда грепнула исходники по menu-hooks (Clipper-меню вызывают .PRG-файлы по имени), отследила, какая программа из какого пункта меню вызывается, сверила с last-modified датами и паттернами обращения к DBF. Получилось, что за 570-файловым «костюмом» прячется бизнес из семи процессов:

  • Ведение клиентов
  • Заведение заказов
  • Выставление счетов: route (счета по маршруту развоза, у дистрибьютора это типичный паттерн) и standard (обычные счета по заявкам)
  • Применение платежей
  • Корректировки склада
  • Приёмка по PO
  • End-of-day постинг

Это перевернуло стратегию миграции: приоритет получили процессы с поведенческой массой, а не объём исходников. Остальные 563 файла — устаревшие отчёты, разовые утилиты вроде «отменить счёт», недоделанные фичи, эксперименты вендора. Тащить их в новую систему значило портировать код, к которому никто не прикасался последние пятнадцать лет.

Чем помог AI и чем не помог

Sprei отдельно выносит роль AI-инструментов в проекте — спокойно, без хайпа. AI ускорил археологию:

  • Сканирование и обзор всех 570 файлов Clipper-исходника.
  • Маппинг программа / меню / DBF.
  • Выделение «кандидатов» на бизнес-правила из императивного процедурного кода — для верификации против современной реализации.
  • Скаффолдинг reconciliation-запросов и progress-документов.

Чего AI не сделал — не заменил верификацию. Каждый вывод грунтовался повторяемым SQL, count-parity, range-проверками и доказательствами в архивах. Гипотезу о purge-паттерне нашли не потому что AI «увидел» это в коде, а потому что один и тот же запрос прогнали сорок раз с разными разрезами и в архивах увидели пустоту там, где ждали историю.

AI был множителем на этапе генерации гипотез, но не оракулом. Дорогая часть legacy-миграции — это не чтение кода, а дисциплинированный verification loop, который ловит случаи, где поведение кода расходится с твоими предположениями.
Joseph Spreiоснователь, Ask the Ledger

Точка принятия решения

У команды было два пути:

  • Делать вид, что данные, которых больше нет, можно восстановить.
  • Квантифицировать ущерб, задокументировать риск и явно классифицировать ситуацию.

Выбрали второй. Reconciliation отгружен с пометкой PASS-with-known-exceptions: 41 095 orphan-строк (1,36%, $530 тысяч) и 4 160 orphan-платежей (1,19%, $342 тысячи) приняты как постоянное legacy-условие с нулевым операционным влиянием на текущий бизнес. Активные клиенты, открытая дебиторская задолженность, текущие платежи — всё, что касается сегодняшней операционной деятельности — сходится идеально. Sprei формулирует развязку коротко: «мы перестали пытаться чинить данные, которых нет».

Частые вопросы
1
Что такое Clipper ERP?

Clipper — диалект xBase, на котором в 80—90-е годы массово писали бизнес-приложения под DOS. Базы — DBF-файлы. Многие Clipper-системы тех лет до сих пор работают в маленьких и средних компаниях, особенно в дистрибуции и оптовой торговле — там, где «работает — не трогай» сильнее, чем «надо обновиться».

2
Почему orphan-строки приняли, а не починили?

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

3
Можно ли было спасти orphan-headers через FK-constraints?

Только если бы они были изначально. В Clipper целостность обеспечивалась логикой приложения, а не БД. Когда headers purged, файлы строк и платежей продолжали жить — потому что никакого FK-constraint не было. На уровне PostgreSQL после миграции FK можно поставить, но это уже про новую схему, а не про восстановление старых данных.

4
Как именно AI ускорил миграцию?

По описанию автора: за пару дней AI обозрел 570 .PRG-файлов и выделил, какие реально вызываются из меню. Та же работа руками — недели. Дальше AI помогал сводить бизнес-правила к тестируемым описаниям и набрасывать reconciliation-запросы. Каждый вывод проходил через ручной SQL-эксперимент: AI как первый pass, человек как gate.

5
Что отсюда забрать в свою legacy-миграцию?

Три практических вывода: первый — usage mapping важнее code volume (большая часть кода может оказаться недостижимым); второй — orphan-данные не всегда баг, иногда это структурная адаптация системы к старым ограничениям; третий — AI ускоряет генерацию гипотез, но verification loop по-прежнему делается руками и SQL-запросами.

Выводы

Кейс Sprei показывает то, что редко звучит в отчётах о legacy-миграциях: данные, которые в новой схеме выглядят поломанными, в старой системе могут оказаться частью дизайна выживания. То, что снаружи похоже на anti-pattern, внутри 40-летней системы было экономически обусловленной адаптацией под несуществующие сегодня ограничения.

Главный методический вывод: миграция legacy ERP — не «переписать код», а аудит того, как этот код прожил столько лет. Перепись — последний шаг. Перед ним — usage mapping (на чём бизнес реально держится), forensics архивов (где история, а где её нет) и принятие того, что часть данных не восстанавливается даже теоретически.

Полный разбор и SQL-цифры — на asktheledger.com. Joseph Sprei — основатель Ask the Ledger, on-premise ERP для wholesale-дистрибьюторов; за плечами — 30+ лет работы с line-of-business-софтом.