Миграция 40-летней Clipper ERP: orphan-строки как способ выжить
Joseph Sprei мигрировал 40-летнюю Clipper ERP и нашёл 41 095 orphan-строк на $872 000. Три фазы расследования: сначала виноватым казался pipeline, потом старый вендор, в финале — оказалось, что система намеренно так устроена и именно так прожила 40 лет.
Если вам предстоит вытащить данные из старой системы и в новой схеме появятся 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:
В цифрах кейса 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, который ловит случаи, где поведение кода расходится с твоими предположениями.
Точка принятия решения
У команды было два пути:
- Делать вид, что данные, которых больше нет, можно восстановить.
- Квантифицировать ущерб, задокументировать риск и явно классифицировать ситуацию.
Выбрали второй. Reconciliation отгружен с пометкой PASS-with-known-exceptions: 41 095 orphan-строк (1,36%, $530 тысяч) и 4 160 orphan-платежей (1,19%, $342 тысячи) приняты как постоянное legacy-условие с нулевым операционным влиянием на текущий бизнес. Активные клиенты, открытая дебиторская задолженность, текущие платежи — всё, что касается сегодняшней операционной деятельности — сходится идеально. Sprei формулирует развязку коротко: «мы перестали пытаться чинить данные, которых нет».
Частые вопросы
Что такое Clipper ERP?
Clipper — диалект xBase, на котором в 80—90-е годы массово писали бизнес-приложения под DOS. Базы — DBF-файлы. Многие Clipper-системы тех лет до сих пор работают в маленьких и средних компаниях, особенно в дистрибуции и оптовой торговле — там, где «работает — не трогай» сильнее, чем «надо обновиться».
Почему orphan-строки приняли, а не починили?
Заголовки счетов, на которые ссылаются orphan-строки, физически удалены и нигде не сохранены. Восстановить их — значит выдумать недостающие данные. Прежний бизнес-смысл был зафиксирован в журнальных проводках общей бухгалтерии и денежных потоках, которые сошлись. Поэтому корректнее принять как факт прошлого, а не имитировать целостность.
Можно ли было спасти orphan-headers через FK-constraints?
Только если бы они были изначально. В Clipper целостность обеспечивалась логикой приложения, а не БД. Когда headers purged, файлы строк и платежей продолжали жить — потому что никакого FK-constraint не было. На уровне PostgreSQL после миграции FK можно поставить, но это уже про новую схему, а не про восстановление старых данных.
Как именно AI ускорил миграцию?
По описанию автора: за пару дней AI обозрел 570 .PRG-файлов и выделил, какие реально вызываются из меню. Та же работа руками — недели. Дальше AI помогал сводить бизнес-правила к тестируемым описаниям и набрасывать reconciliation-запросы. Каждый вывод проходил через ручной SQL-эксперимент: AI как первый pass, человек как gate.
Что отсюда забрать в свою 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-софтом.