Пустая строка вместо NULL
Пустая строка вместо NULL — что чинили в ночь на 20 августа
Хаб A-2022BAYAREA · база E:\Obsidian\_imports\leads.db · цифры сняты с живой базы и с бэкапа 13:58 pre-vcround-null-fix
Главное: насколько врал прибор
81 537строк в vc_round
81 537читатель с IS NOT NULL считал заполненными — было, это 100%
8 062реально пригодны для анализа (9.9%) — столько же он видит сейчас
73 475 → 0строк, про которые прибор врал
Одной картинкойбыло: «данные есть» на всю таблицу
стало: «данные есть» ровно там, где они есть
Писателей у vc_* было четыре, а починили три
| Скрипт | Что писал | Когда починен |
build_vc_graph.py | f.get(key, '') на пустых полях Airtable | 19.08 днём, дневная сессия |
build_vc_graph2.py | литеральные '' в ins_round (forms-rounds, signal) | 19.08 днём, дневная сессия |
build_vc_graph3.py | литеральные '' для архивных раундов | 19.08 днём, дневная сессия |
warm_tw_bridge.py | SET tg_handle='', tg_src='' — чистка своих прошлых вставок, рутина 04:15 | 19.08 ночью, эта сессия |
Почему четвёртый важнее самой находки. Дневная починка была правильной и по корню. Но обход писателей шёл по имени роли — искали скрипты build_*, — а не по факту записи в таблицу. Четвёртый писатель это отдельная ночная рутина «VC Nightly Warm Refresh» в 04:15: она вернула бы 2 452 пустых ячейки примерно через четыре часа после того, как починку объявили закрытой. Контрфакт на копии 500 строк: старый SQL даёт 500 пустых ячеек, новый — 0. Живой прогон патченого скрипта: 1,2 с, 2 452 привязки на месте, реальные хендлы (2 614 и 5 847) целы, пустых ячеек 0.
⛔ Что сознательно не тронуто
Та же болезнь шире: 9 495 340 пустых ячеек в 8 таблицах, у них 16 живых писателей. Трогать их сегодня нельзя — там '' местами несёт смысл: leads.last_contact='' означает «ни разу не контактировали» (5 469 строк), и alpha/leadsignal_scan.py отбирает дормантных инвесторов ровно по last_contact = ''. Замена на NULL молча выкинула бы эти строки из сигнала. Сначала разбор писателей и читателей, потом данные.
| Таблица | строк | пустых ячеек | худшие колонки | писатели |
airtable_records | 907 456 | 5 154 361 | handle 893 754 · email 876 936 · role 867 065 | fold_delta.py · load_airtable_to_db.py |
archive_contacts | 312 254 | 2 001 169 | company 300 376 · phone 299 929 · email 236 475 | gdrive/wire_archive_contacts.py |
people | 292 510 | 1 462 368 | vault_slug 283 213 · category 280 040 · status 280 039 | build_unified_sql.py · dedup_people.py · dialogs/match_members_to_crm.py · fb_audience.py |
companies | 88 549 | 470 333 | location 88 441 · linkedin 88 245 · telegram 85 322 | build_unified_sql.py |
contacts | 80 789 | 247 409 | org 77 254 · person_note 76 370 · relationship 76 370 | apple-contacts/build_contacts_db.py · browser-history/contacts_to_db.py · build_leads_db.py · gdrive/gold_extract.py |
vc_contact_match | 123 402 | 115 811 | note 115 811 | build_warm_matches.py |
leads | 14 135 | 36 177 | country 7 450 · crm_tg_id 5 995 · first_contact 5 470 | build_leads_db.py · crm-engine/drip.py · listener.py · retag.py · retag_v2.py |
works_at | 36 809 | 7 712 | role 7 712 | build_unified_sql.py |
Что теперь сторожит
E:\Obsidian\_imports\_test_sql_hygiene.py — три проверки, ноль LLM, ноль сети:
- unit нормализатора
_sql_hygiene.null_empty_text на синтетической базе, включая идемпотентность и skip_cols; - статический запрет: ни один скрипт в
_imports не присваивает '' в колонку vc_*; - живая инварианта: в
leads.db в таблицах vc_* ноль пустых строк.
красный тест пройден подложенный старый писатель ловится, выход 1.
панель ломателей три семьи моделей через OpenRouter, все возражения проверены по коду и закрыты.
Честная граница сторожа: живая проверка покрывает только vc_*. Восемь таблиц выше он не сторожит — там сначала нужен разбор писателей.
Собрано автоматически 19.08.2026 23:46 · бэкапы: _backups\leads.db.pre-warmbridge-nullfix-20260819 (1,33 ГБ), _backups\warm_tw_bridge.py.bak-20260819-233805