Esquema de la base de datos
El corpus de IRIS Core es SQL plano (sin ORM): las migraciones bajo
../db/migrations/ son la única fuente de verdad. Esta página es una
referencia legible por humanos generada a partir de esas migraciones. El corpus es neutral — cero campos ni
referencias cruzadas de RMC (impuesto por pnpm neutrality).
- Motor: PostgreSQL 16 +
pgvector (halfvec + HNSW), pg_trgm, fuzzystrmatch, unaccent,
búsqueda de texto completo en español.
- Herramienta de migración:
dbmate (cada archivo tiene -- migrate:up / -- migrate:down).
Diagrama entidad–relación
erDiagram
instancia ||--o{ instancia_clase : "classes"
instancia ||--o{ instancia_persona : "owners/agents"
instancia ||--o{ anotacion : "events"
instancia |o--o| instancia : "renewal chain"
persona ||--o{ instancia_persona : "role"
clase ||--o{ instancia_clase : ""
estado ||--o{ instancia : "status"
tipo_signo ||--o{ instancia : "sign type"
tipo_anotacion ||--o{ anotacion : "type"
pais ||--o{ persona : "country"
raw_source ||--o{ dead_letter : "re-derive handle"
consumer ||--o{ api_key : "owns"
Corpus (entidades neutrales)
instancia — la solicitud de marca
La entidad central. Clave natural nro_solicitud (D-01); denominacion es un atributo, nunca una
clave (D-02). Enriquecida de forma autoritativa por el Buscador; sembrada (solo nulos) por Sheets.
| Columna |
Type |
Notas |
nro_solicitud |
text |
PK — clave natural |
nro_registro |
text |
aparece solo al otorgarse, nullable |
denominacion |
text |
NOT NULL — el texto de la marca |
tipo_signo |
text |
→ tipo_signo(codigo) |
estado |
text |
→ estado(codigo) |
tipo_nombre, subtipo_nombre |
text |
tipo/subtipo de INAPI (D-14) |
fecha_presentacion, fecha_publicacion, fecha_registro, fecha_vencimiento |
date |
las cuatro fechas explícitas (D-14) |
traduccion, descripcion_etiqueta, protection_description, imagen_url |
text |
atributos de detalle (D-14) |
renovada_de, renovada_por |
text |
self-FK → instancia(nro_solicitud) — cadena de renovación (D-17) |
embedding |
halfvec(1024) |
vector semántico, índice HNSW |
search_vector |
tsvector |
generado — FTS en español sobre unaccent(denominacion) |
content_hash |
text |
idempotencia (INGEST-05) |
created_at, updated_at |
timestamptz |
|
persona — titulares y agentes
| Columna |
Type |
Notas |
id |
bigint |
PK (identidad) |
pais |
text |
→ pais(codigo) |
identificador |
text |
RUT (Chile) o id extranjero, nullable |
nombre, apellido |
text |
|
region, comuna |
text |
opcional (D-16) |
search_vector |
tsvector |
generado, FTS |
created_at |
timestamptz |
|
Clave única uq_persona_pais_identificador (pais, identificador) donde identificador IS NOT NULL;
las personas sin id se reconcilian con mejor esfuerzo por (pais, lower(nombre)).
instancia_persona — puente de rol
| Columna |
Type |
Notas |
instancia_nro_solicitud |
text |
PK, → instancia |
persona_id |
bigint |
PK, → persona |
rol |
text |
titular | representante | ambas |
instancia_clase — clases de Niza
| Columna |
Type |
Notas |
instancia_nro_solicitud |
text |
PK, → instancia |
clase_numero |
int |
PK, → clase (1..45) |
descripcion, estado |
text |
descripción/estado por clase |
anotacion — eventos del Estado-Diario (solo-anexado)
| Columna |
Type |
Notas |
id |
bigint |
PK (identidad) |
instancia_nro_solicitud |
text |
→ instancia (un padre faltante se convierte en stub/se pone en cuarentena) |
tipo |
text |
→ tipo_anotacion(codigo) — M1..M14 |
fecha, fecha_vencimiento |
date |
|
observacion, seccion, seccion_nombre |
text |
|
created_at |
timestamptz |
|
Clave de deduplicación uq_anotacion_event (instancia_nro_solicitud, tipo, fecha, md5(coalesce(observacion,'')))
— un evento repetido es un DO NOTHING limpio (los eventos nunca mutan).
Catálogos
| Tabla |
PK |
Otros |
estado |
codigo |
nombre — conjunto controlado de estados |
tipo_signo |
codigo |
nombre |
tipo_anotacion |
codigo |
seccion_nombre — M1..M14 |
pais |
codigo |
nombre — conjunto ISO completo + códigos obsoletos |
clase |
numero |
nombre — clases de Niza, CHECK (numero BETWEEN 1 AND 45) |
Un valor de catálogo desconocido se acepta como null + se registra en log (D-09) — un nuevo valor de INAPI nunca detiene
la ingesta.
Ingesta / operaciones
| Tabla |
Clave |
Propósito |
sync_state |
source (PK) |
cursor de reanudación por fuente (cursor jsonb) |
movement_log |
id |
feed de cambios solo-anexado (entity_type, entity_id, change_type, source, occurred_at, content_hash) — alimenta la lista de trabajo del Buscador |
raw_source |
id |
bytes en crudo retenidos (storage_path bajo RAW_STORAGE_DIR, content_type, size) — re-derivabilidad (D-10) |
dead_letter |
id |
cuarentena (source, reason, raw_source_id → raw_source, payload jsonb) — no aborta (D-07) |
sync_run |
id |
historial de ejecuciones (source, started_at, finished_at, status ok\|failed\|running, processed, changed, quarantined, error) — OBS-03 |
Control de acceso
| Tabla |
Clave |
Propósito |
consumer |
id |
organización/tenant (name UNIQUE) |
api_key |
id |
consumer_id → consumer; key_hash (sha256, UNIQUE — nunca en texto plano), prefix, scopes text[], monthly_quota int (>= 0), expires_at, rotated_at, revoked_at |
Scopes: brands:read (/v1/brands, MCP search_brands/get_brand_detail), insights:read
(/v1/freshness, /v1/sync-runs). Scopes vacíos = sin restricción. Los contadores de cuota viven en Redis
(compartidos REST ↔ MCP), no en Postgres, de modo que la ruta de lectura se mantiene solo-SELECT.
Índices clave
- FTS: GIN sobre
instancia.search_vector y persona.search_vector (español, insensible a acentos
vía unaccent).
- Fuzzy:
pg_trgm GIN/GiST sobre la denominación para búsqueda tolerante a errores tipográficos.
- Vector: HNSW sobre
instancia.embedding (halfvec, coseno) para similitud semántica.
- Reconcile: parcial
idx_persona_noident_reconcile (pais, lower(nombre)) WHERE identificador IS NULL.
- Claves naturales / dedup:
uq_anotacion_event, uq_persona_pais_identificador, más las PKs de las tablas anteriores.
Las migraciones se aplican vía dbmate up; el DDL completo (índices, columnas generadas, orden de FK,
configuración de FTS en español) vive en ../db/migrations/.