feat: DuckDB como fuente de verdad (multi-valor, ownership selectivo, escritura, libretas)
F1 — migraciones: 002_multivalue (persons +telefonos/emails/direcciones/extra_fm JSON,
backfill desde singulares con to_json) + 003_addressbooks (tabla addressbooks + seed
idempotente de la libreta por defecto). Conteos intactos (697/1065/98).
F2 — ingest_vault selectivo (anti-pisado): personas que ya existen en DB solo actualizan
note_path + extra_fm vía duckdb_upsert(update_cols=...), NO pisan los campos OWNED por la
DB; personas nuevas = bootstrap completo. _link_contacts enlaza por listas telefonos[]/
emails[] además del singular. ingest_dav itera todas las libretas de la tabla addressbooks.
F3 — escritura estructurada (server/writes.py + endpoints en main.py): CRUD
/api/person|contact|event, /api/addressbook, /api/calendar, /api/person/{slug}/render
(DB→nota preservando la prosa del cuerpo), /api/push/dav (reconcilia DB→Xandikos). El push
DAV y el render ocurren fuera de la transacción de escritura para no bloquear la DB con
latencia de red. registry_bridge.py importa las funciones nuevas; app.md actualizado.
Verificado: 18 tests verdes; ownership probado sobre datos reales (un centinela DB-owned
sobrevivió a POST /api/ingest/vault sobre las 697 fichas); person CRUD + materialización
de la ficha .md en vivo, con cleanup sin residuo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
-- Migración 003: tabla de libretas CardDAV (addressbooks).
|
||||
--
|
||||
-- La DB es la fuente de verdad de las libretas de contactos: el ingest DAV
|
||||
-- itera todas las libretas registradas aquí (no solo la colección fija) y cada
|
||||
-- contacto guarda su collection real. Los endpoints de escritura crean libretas
|
||||
-- nuevas en Xandikos y las registran aquí.
|
||||
--
|
||||
-- Aditiva e idempotente: CREATE TABLE IF NOT EXISTS + seed ON CONFLICT DO NOTHING.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS addressbooks (
|
||||
slug VARCHAR PRIMARY KEY,
|
||||
display_name VARCHAR,
|
||||
collection_path VARCHAR,
|
||||
description VARCHAR,
|
||||
color VARCHAR,
|
||||
created_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
|
||||
-- Seed idempotente de la libreta por defecto (la que apunta config.py por
|
||||
-- defecto). Re-aplicar la migración no la duplica.
|
||||
INSERT INTO addressbooks (slug, display_name, collection_path, description, color)
|
||||
VALUES ('addressbook', 'Contactos', '/enmanuel/contacts/addressbook/', NULL, NULL)
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
Reference in New Issue
Block a user