Cursor разбирается с кодом
Cursor разбирается с кодом

Началось все весьма прозаично, клиент позвонил к нам в техподдержку и спросил «а как бы мне поставить ваш софт но в другую схему БД». Собственно вопрос проще некуда — мы писали на спринге, а значит лезем в application.yml и ставим схему. Но, клиент не из тупых и уже это попробовал — не сработало.

Начинаем разбираться что сломалось и кто виноват. Первым делом ДевОпс повторяет кульбиты клиента и выдает простой вердикт: «В 151 миграции лажа». Я открываю и: «батюшки родный, да это же лосенок явное указание схемы!»

Все оказалось примитивно и просто, когда‑то давно, примерно год назад, я продолбал упустил эту ошибку на код‑ревью. Уж не знаю чем я тогда думал и почему пропустил, но за все надо когда‑то платить. Собственно покуда мы отдаем всю базу в распоряжение, схема была просто public, как можно догадаться искать по слову «public» в Java это все равно, что искать воду на берегу моря — найдешь много, да все не то.

Тут я вспоминаю, что у меня же есть новенький джун Курсор! Иду в агента и говорю: «А найди ка мне все упоминания схемы public во всех файлах». Отправляю и тут до меня доходит, что он может сейчас начать заливать все файлы по очереди в LLM дабы проанализировать их, что неминуемо вышибет его из контекста, но — нет! Вместо этого он пишет несколько скриптов и радостно их запускает (с моего одобрения, само собой). Он проверил все миграции, все SQL файлы вообще, все упоминания JPA Query и даже конкатенации строк в которых было SELECT | INSERT | UPDATE. В общем‑то молодец Курсоша, ничего не забыл.

Вердикт был скуп - виновата ОДНА миграция, во всех прочих местах обращение идет без упоминания схемы, как и требовалось по изначальной задумке.

Для миграций мы используем FlyWay, он простой, надежный и отлично интегрирован в различные платформы и языки. Каждый раз, когда накатывается новая миграция по ней считается контрольная сумма, которая сохраняется в БД в служебную таблицу. И не дай Бог вам потом поменять хоть букву в старой миграции! При запуске миграции не накатятся, запуск обвалится с ошибкой. Да, это можно отключить, но чревато тем, что шаловливые ручки клиентов понапишут что попало.

Вариантов было несколько:

  1. Правим миграцию и всем клиентам отправляем "UPDATE ... WHERE ..." и слезно просим выполнить этот SQL перед очередным обновлением.

  2. Правим миграцию в отдельной ветке под этого клиента.

  3. Ничего не правим, клиентом больше - клиентом меньше.

Как можно догадаться все варианты ужасают последствиями, хотелось бы и на елку влезть и рыбку съесть. И тут я вспоминаю, что у меня же есть новенький джун Курсор! (где-то это уже было) И говорю ему "А давай посмотрим, как FlyWay считает контрольную сумму, Иди в интернет и ищи как он это делает, нам нужна реализация".

Идея была проста как биткойн, нужно было добавляя что-то безопасное для SQL (да, правильно, комментарий) добиться совпадения контрольной суммы с исходной. Брутфорсер CRC32 не самая сложная штука, диапазон невелик, можно легко перебрать все варианты.

Он находит исходники, пишет код, но его код не работает. Тогда мы берем готовую библиотеку и через reflection цепляемся к механизму генерации checksum. Все получилось — ура! Сумма совпала, но есть проблема — сумма считается по файлу, и курсор написал код, который делает временный файл, а потом скармливает его функции. Перебор всех вариантов AKA Bruteforce возможен, но чертовский медленно это делает.

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

Но CRC не имеет метода clone или set, и снова reflection нас спасает! Для начала пишем простую генерацию, добавляем последовательно 4 байта перебирая их (тупой цикл int) — нужно было понять насколько сложно это сделать. Оказалось — не сложно, за 1–3 секунды находится подходящая последовательность.

Идея — 1, провальная.

Мы находим 4 байта, а потом к ним (так же перебором) находим строку начинающуюся на -‑ и далее перебираем печатные символы (буквы, цифры, спецсимволы) проверяя CRC наших 4 байт и этой строки. Идея была проста, раз CRC строки совпадает, то и Update(строка) совпадет. Но — нет: ) не совпал.

Идея — 2, которая сработала.

Выкидываем перебор 4 байт и делаем точно так же сразу перебор печатной строки.

Итого:

  • Считаем старую миграцию - понимаем куда стремиться.

  • Считаем новую миграцию — берем базис

  • Перебираем строки начиная от -‑ и далее по буквам 1,2,3,4,5,6...

Как итог, у нас получился достаточно быстрый брутфорсер миграций, который позволил нам изменить старую миграцию так, что контрольная сумма осталась. Теперь новые клиенты будут иметь дело с новым SQL, а старые ничего не заметят, для них все как было «нормально», так «нормально» и останется.

Я долго думал, не нарушает ли этот брутфорсер чьих‑то прав, и этично ли он устроен, но ничего не придумал. Вроде как ничего опасного или противозаконного с его помощью сделать нельзя, а потому просто выложил его на всеобщее обозрение:

Вот тут

Суть в том, что это был первый раз, когда я оценил мощь ИИ, мне было бы лень ковырять декомпилированный текст из‑за всех этих тупых val1, val2, fun1 и тому подобное Лично для меня Курсор больше всего похож на очень начитанного джуна, такого «студента‑отличника без опыта работы». Всю черную работу «сделай ребэйз», «давай переименуем с com.factory.stable на org.fucktory.last» — все это я смело поручаю ему и он с этим справляется. Пока он весьма туп в глубоком анализе и запросто может наговнокодить, но для этого и есть мы — кожаные.

На этом все, спасибо за внимание.

P.S.: Да, нам он помог, мы таки поменяли миграцию и все остались довольны.

Комментарии (4)


  1. corefly
    29.10.2025 02:00

    ну то есть фактически обманули валидатор. Даже не знаю, плакать или смеяться :)


    1. Sharque Автор
      29.10.2025 02:00

      Ну фактически да, но в этом и был смысл. Насколько я понял он вообще больше нужен как защита от кривых рук и от дурака. А нам "ну надо..."


  1. SimSonic
    29.10.2025 02:00

    В liquibase-то это устроено сразу, можно явно указать, какой хэш будет у текущего ченджсета ...


    1. Sharque Автор
      29.10.2025 02:00

      Статья не о "Смотрите, какие мы молодцы, сами накосячили, сами все поправили" и не о том как работать с FlyWay и почему он лучше / хуже. Статья о Cursor - о том, что он умеет в декомпиляцию и не считает это "ай-яй-яй" и о том, что он может справляться с задачами посложнее школьной информатики.