Skip to content

Database schema

The IRIS Core corpus is plain SQL (no ORM): the migrations under ../db/migrations/ are the single source of truth. This page is a human-readable reference generated from those migrations. The corpus is neutral — zero RMC fields or cross-references (enforced by pnpm neutrality).

  • Engine: PostgreSQL 16 + pgvector (halfvec + HNSW), pg_trgm, fuzzystrmatch, unaccent, Spanish full-text search.
  • Migration tool: dbmate (each file has -- migrate:up / -- migrate:down).

Entity–relationship diagram

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 (neutral entities)

instancia — the trademark application

The central entity. Natural key nro_solicitud (D-01); denominacion is an attribute, never a key (D-02). Enriched authoritatively by the Buscador; seeded (nulls-only) by Sheets.

Column Type Notes
nro_solicitud text PK — natural key
nro_registro text appears only on grant, nullable
denominacion text NOT NULL — the mark text
tipo_signo text tipo_signo(codigo)
estado text estado(codigo)
tipo_nombre, subtipo_nombre text INAPI tipo/subtipo (D-14)
fecha_presentacion, fecha_publicacion, fecha_registro, fecha_vencimiento date the four explicit dates (D-14)
traduccion, descripcion_etiqueta, protection_description, imagen_url text detail attributes (D-14)
renovada_de, renovada_por text self-FK → instancia(nro_solicitud) — renewal chain (D-17)
embedding halfvec(1024) semantic vector, HNSW index
search_vector tsvector generated — Spanish FTS over unaccent(denominacion)
content_hash text idempotency (INGEST-05)
created_at, updated_at timestamptz

persona — holders & agents

Column Type Notes
id bigint PK (identity)
pais text pais(codigo)
identificador text RUT (Chile) or foreign id, nullable
nombre, apellido text
region, comuna text optional (D-16)
search_vector tsvector generated, FTS
created_at timestamptz

Unique key uq_persona_pais_identificador (pais, identificador) where identificador IS NOT NULL; no-id personas reconcile best-effort by (pais, lower(nombre)).

instancia_persona — role bridge

Column Type Notes
instancia_nro_solicitud text PK, → instancia
persona_id bigint PK, → persona
rol text titular | representante | ambas

instancia_clase — Nice classes

Column Type Notes
instancia_nro_solicitud text PK, → instancia
clase_numero int PK, → clase (1..45)
descripcion, estado text per-class description/status

anotacion — Estado-Diario events (append-only)

Column Type Notes
id bigint PK (identity)
instancia_nro_solicitud text instancia (a missing parent is stubbed/quarantined)
tipo text tipo_anotacion(codigo) — M1..M14
fecha, fecha_vencimiento date
observacion, seccion, seccion_nombre text
created_at timestamptz

Dedup key uq_anotacion_event (instancia_nro_solicitud, tipo, fecha, md5(coalesce(observacion,''))) — a repeated event is a clean DO NOTHING (events never mutate).


Catalogs

Table PK Other
estado codigo nombre — controlled status set
tipo_signo codigo nombre
tipo_anotacion codigo seccion_nombre — M1..M14
pais codigo nombre — full ISO set + deprecated codes
clase numero nombre — Nice classes, CHECK (numero BETWEEN 1 AND 45)

An unknown catalog value is accepted as null + logged (D-09) — a new INAPI value never halts ingestion.


Ingest / operations

Table Key Purpose
sync_state source (PK) per-source resume cursor (cursor jsonb)
movement_log id append-only change feed (entity_type, entity_id, change_type, source, occurred_at, content_hash) — drives the Buscador work-list
raw_source id retained raw bytes (storage_path under RAW_STORAGE_DIR, content_type, size) — re-derivability (D-10)
dead_letter id quarantine (source, reason, raw_source_idraw_source, payload jsonb) — non-aborting (D-07)
sync_run id run history (source, started_at, finished_at, status ok\|failed\|running, processed, changed, quarantined, error) — OBS-03

Access control

Table Key Purpose
consumer id org/tenant (name UNIQUE)
api_key id consumer_idconsumer; key_hash (sha256, UNIQUE — never plaintext), 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). Empty scopes = unrestricted. Quota counters live in Redis (shared REST ↔ MCP), not Postgres, so the read path stays SELECT-only.


Key indexes

  • FTS: GIN on instancia.search_vector and persona.search_vector (Spanish, accent-insensitive via unaccent).
  • Fuzzy: pg_trgm GIN/GiST on denomination for typo-tolerant search.
  • Vector: HNSW on instancia.embedding (halfvec, cosine) for semantic similarity.
  • Reconcile: partial idx_persona_noident_reconcile (pais, lower(nombre)) WHERE identificador IS NULL.
  • Natural keys / dedup: uq_anotacion_event, uq_persona_pais_identificador, plus the table PKs above.

Migrations are applied via dbmate up; the full DDL (indexes, generated columns, FK ordering, Spanish FTS config) lives in ../db/migrations/.