# Modelo de datos completo de Senza Paura Generado el 2026-09-14T20:26:39.479Z desde el esquema PostgreSQL aplicado y contrastado con los contratos TypeScript de la aplicación. **Alcance:** 83 tablas, 1 vista, 3 secuencias, 4 funciones de base y 42 migraciones aplicadas. ## Fuentes y convenciones - Esquema declarativo: `src/lib/server/db/schema/`. - Historial físico: `drizzle/`. - Campos TypeScript en `camelCase`; nombres PostgreSQL en `snake_case`. - `text` es el identificador opaco habitual. `pais.codigo` es la excepción: usa ISO 3166-1 alfa-2. - Los importes monetarios son enteros en céntimos y la moneda usa código ISO en minúsculas. - Las marcas de tiempo son `timestamp with time zone`. - Los estados editoriales comunes son `borrador`, `publicado` y `retirado`. ## Vista conceptual ```mermaid flowchart LR BASE[Base y artistas] --> CAT[Catálogo musical] CAT --> INT[Interacción y escucha] CTA[Cuentas y suscripciones] --> INT CTA --> COM[Comercio] CAT --> COM COM --> FAC[Facturación] CAT --> EDI[Editorial y legal] BASE --> TALLER[Taller de letras] CAT --> TALLER CTA --> JUKE[Jukebox] CAT --> JUKE ``` La entidad central es `cancion`. Se relaciona con álbumes, estilos, intérpretes, audios, autoría, etiquetas, ventas, descargas, listas, reproducciones, likes/valoraciones, entradas editoriales, géneros del taller y prompts. ## Reglas que definen el modelo - Una versión musical es otra fila de `cancion`; `version_de_id` apunta a la canción principal. Una versión no puede venderse por separado. - `valoracion` tiene una sola fila por `(usuario_id, cancion_id)`: `like` y `value` no se pisan. Quitar un like conserva `value` y `date`. - `artista` es la identidad pública; `persona` es la persona física o acreditada. `artista_persona` permite solistas, grupos y proyectos. - `compra` materializa el permiso de descarga. El pedido conserva el hecho comercial; la factura conserva el hecho fiscal. - Una factura emitida no se reescribe: las correcciones se modelan como facturas rectificativas. - La programación del Jukebox vive como franjas JSON versionadas mediante `revision` para control de concurrencia. - El taller usa vocabularios normalizados y tablas puente para géneros, países, metros, compases, instrumentos, figuras, obras, versos y glosario. ## Relaciones principales ```mermaid erDiagram USUARIO ||--o{ VALORACION : emite CANCION ||--o{ VALORACION : recibe USUARIO ||--o{ FAVORITO : guarda USUARIO ||--o{ LISTA_REPRODUCCION : crea LISTA_REPRODUCCION ||--o{ LISTA_REPRODUCCION_CANCION : contiene CANCION ||--o{ LISTA_REPRODUCCION_CANCION : aparece ESTILO ||--o{ ALBUM : agrupa ESTILO ||--o{ CANCION : clasifica ALBUM ||--o{ CANCION : publica CANCION ||--o{ CANCION_AUDIO : dispone MEDIO ||--o{ CANCION_AUDIO : almacena ARTISTA ||--o{ CANCION_INTERPRETE : interpreta CANCION ||--o{ CANCION_INTERPRETE : acredita USUARIO ||--o{ PEDIDO : realiza PEDIDO ||--o{ PEDIDO_LINEA : contiene PEDIDO ||--o{ FACTURA : documenta USUARIO ||--o{ JUKEBOX : configura ``` ## Contratos JSON de la aplicación ### Contenido editorial por bloques `artista.secciones`, `album.secciones`, `cancion.inspiracion_secciones` y `entrada.secciones` contienen una lista ordenada de bloques: - Base: `{ id, tipo, titulo? }`. - Texto: `{ tipo: "texto", texto }`. - Código: `{ tipo: "codigo", codigo, lenguaje? }`. - Medio: `{ tipo: "video" | "imagen" | "audio", url, pie?, subtitulos?, alt? }`. ### Presentación editorial `artista.presentacion`, `album.presentacion`, `cancion.presentacion` y `entrada.presentacion` usan `{ plantilla, heroe }`, donde `heroe` es `automatico`, `visible` u `oculto`. ### Jukebox `jukebox.franjas` y `programacion_jukebox.franjas` contienen objetos `{ id, inicio, fin, orden, catalogo, etiquetas?, contenidos }`. `orden` es `secuencial` o `aleatoria`; `contenidos` referencia listas o canciones mediante `{ tipo, id }`; varias etiquetas se combinan como unión. ### Comercio abierto `producto.datos`, `pedido_linea.datos`, `intencion_pago.datos` y `factura.datos_congelados` conservan atributos variables o una instantánea del momento comercial. Los campos consultables y las cantidades monetarias permanecen normalizados en columnas. ## Base y artistas ### `artista` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | -------------------- | -------------------------- | :--: | ----------------------------------------------------------------------------------------- | | `id` | `text` | no | — | | `codigo` | `text` | no | `('ART-'::text \|\| lpad((nextval('artista_codigo_seq'::regclass))::text, 6, '0'::text))` | | `slug` | `text` | no | — | | `tipo` | `text` | no | `'persona'::text` | | `nombre` | `text` | no | — | | `descripcion_corta` | `text` | no | `''::text` | | `biografia` | `text` | no | `''::text` | | `imagen_id` | `text` | sí | — | | `pais_codigo` | `character(2)` | sí | — | | `lugar_origen` | `text` | sí | — | | `inicio` | `integer` | sí | — | | `fin` | `integer` | sí | — | | `sitio_web` | `text` | sí | — | | `estado` | `text` | no | `'borrador'::text` | | `origen` | `text` | no | `'manual'::text` | | `modelo_origen` | `text` | sí | — | | `importado_en` | `timestamp with time zone` | sí | — | | `revisado_por` | `text` | sí | — | | `revisado_en` | `timestamp with time zone` | sí | — | | `creado_en` | `timestamp with time zone` | no | `now()` | | `actualizado_en` | `timestamp with time zone` | sí | — | | `secciones` | `jsonb` | no | `'[]'::jsonb` | | `presentacion` | `jsonb` | no | `'{}'::jsonb` | | `es_virtual` | `boolean` | no | `false` | | `nombre_visual` | `text` | no | `'texto'::text` | | `logotipo_id` | `text` | sí | — | | `logotipo_claro_id` | `text` | sí | — | | `logotipo_oscuro_id` | `text` | sí | — | Claves y restricciones: - **CHECK artista_estado_conocido:** `CHECK (estado = ANY (ARRAY['borrador'::text, 'publicado'::text, 'retirado'::text]))` - **CHECK artista_nombre_visual_conocido:** `CHECK (nombre_visual = ANY (ARRAY['texto'::text, 'logotipo'::text]))` - **CHECK artista_origen_conocido:** `CHECK (origen = ANY (ARRAY['manual'::text, 'importado'::text]))` - **CHECK artista_periodo_valido:** `CHECK (fin IS NULL OR inicio IS NULL OR fin >= inicio)` - **CHECK artista_presentacion_objeto:** `CHECK (jsonb_typeof(presentacion) = 'object'::text)` - **CHECK artista_secciones_array:** `CHECK (jsonb_typeof(secciones) = 'array'::text)` - **CHECK artista_tipo_conocido:** `CHECK (tipo = ANY (ARRAY['persona'::text, 'grupo'::text, 'proyecto'::text, 'virtual'::text]))` - **FK artista_imagen_fk:** `FOREIGN KEY (imagen_id) REFERENCES medio(id) ON DELETE SET NULL` - **FK artista_logotipo_claro_id_medio_id_fk:** `FOREIGN KEY (logotipo_claro_id) REFERENCES medio(id) ON DELETE SET NULL` - **FK artista_logotipo_id_medio_id_fk:** `FOREIGN KEY (logotipo_id) REFERENCES medio(id) ON DELETE SET NULL` - **FK artista_logotipo_oscuro_id_medio_id_fk:** `FOREIGN KEY (logotipo_oscuro_id) REFERENCES medio(id) ON DELETE SET NULL` - **FK artista_pais_fk:** `FOREIGN KEY (pais_codigo) REFERENCES pais(codigo)` - **PK artista_pkey:** `PRIMARY KEY (id)` Índices adicionales: `artista_codigo_unico`, `artista_estado`, `artista_nombre`, `artista_slug_unico`. ### `artista_identificador` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `artista_id` | `text` | no | — | | `sistema` | `text` | no | — | | `valor` | `text` | no | — | Claves y restricciones: - **FK artista_identificador_artista_fk:** `FOREIGN KEY (artista_id) REFERENCES artista(id) ON DELETE CASCADE` - **PK artista_identificador_pk:** `PRIMARY KEY (artista_id, sistema)` Índices adicionales: `artista_identificador_externo_unico`. ### `artista_persona` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `artista_id` | `text` | no | — | | `persona_id` | `text` | no | — | | `relacion` | `text` | no | — | | `desde` | `integer` | sí | — | | `hasta` | `integer` | sí | — | | `orden` | `integer` | no | `0` | Claves y restricciones: - **CHECK artista_persona_periodo_valido:** `CHECK (hasta IS NULL OR desde IS NULL OR hasta >= desde)` - **CHECK artista_persona_relacion_conocida:** `CHECK (relacion = ANY (ARRAY['identidad'::text, 'miembro'::text, 'fundador'::text]))` - **FK artista_persona_artista_fk:** `FOREIGN KEY (artista_id) REFERENCES artista(id) ON DELETE CASCADE` - **FK artista_persona_persona_fk:** `FOREIGN KEY (persona_id) REFERENCES persona(id) ON DELETE CASCADE` - **PK artista_persona_pk:** `PRIMARY KEY (artista_id, persona_id, relacion)` Índices adicionales: `artista_persona_persona`. ### `medio` Archivo administrado: imagen, audio, muestra o documento, con metadatos técnicos e integridad SHA-256. | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | -------------------------- | :--: | ----------------- | | `id` | `text` | no | — | | `clase` | `text` | no | — | | `clave` | `text` | no | — | | `publico` | `boolean` | no | `false` | | `mime` | `text` | no | — | | `bytes` | `integer` | no | — | | `duracion_s` | `integer` | sí | — | | `ancho` | `integer` | sí | — | | `alto` | `integer` | sí | — | | `sha256` | `text` | no | — | | `alt` | `text` | sí | — | | `creado_en` | `timestamp with time zone` | no | `now()` | Claves y restricciones: - **PK medio_pkey:** `PRIMARY KEY (id)` - **UNIQUE medio_clave_unique:** `UNIQUE (clave)` Índices adicionales: `medio_clase`, `medio_sha`. ### `pais` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `codigo` | `character(2)` | no | — | | `nombre` | `text` | no | — | | `gentilicio` | `text` | sí | — | | `region` | `text` | sí | — | Claves y restricciones: - **PK pais_pkey:** `PRIMARY KEY (codigo)` ### `persona` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ---------------- | -------------------------- | :--: | ------------------ | | `id` | `text` | no | — | | `slug` | `text` | no | — | | `nombre` | `text` | no | — | | `nombre_legal` | `text` | sí | — | | `ipi` | `text` | sí | — | | `nacimiento` | `integer` | sí | — | | `defuncion` | `integer` | sí | — | | `pais_codigo` | `character(2)` | sí | — | | `retrato_id` | `text` | sí | — | | `semblanza` | `text` | sí | — | | `estado` | `text` | no | `'borrador'::text` | | `creado_en` | `timestamp with time zone` | no | `now()` | | `actualizado_en` | `timestamp with time zone` | sí | — | | `origen` | `text` | no | `'manual'::text` | | `modelo_origen` | `text` | sí | — | | `importado_en` | `timestamp with time zone` | sí | — | | `revisado_por` | `text` | sí | — | | `revisado_en` | `timestamp with time zone` | sí | — | Claves y restricciones: - **FK persona_pais_codigo_pais_codigo_fk:** `FOREIGN KEY (pais_codigo) REFERENCES pais(codigo)` - **FK persona_retrato_id_medio_id_fk:** `FOREIGN KEY (retrato_id) REFERENCES medio(id) ON DELETE SET NULL` - **PK persona_pkey:** `PRIMARY KEY (id)` - **UNIQUE persona_slug_unique:** `UNIQUE (slug)` Índices adicionales: `persona_nombre`, `persona_pais`. ## Catálogo musical ### `album` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | --------------------- | -------------------------- | :--: | ------------------ | | `id` | `text` | no | — | | `slug` | `text` | no | — | | `titulo` | `text` | no | — | | `estilo_id` | `text` | no | — | | `tipo` | `text` | no | `'album'::text` | | `fecha` | `text` | no | — | | `portada_id` | `text` | sí | — | | `sello` | `text` | sí | — | | `resumen` | `text` | no | `''::text` | | `destacado` | `boolean` | no | `false` | | `numero_catalogo` | `text` | sí | — | | `formatos` | `text[]` | sí | — | | `pais_edicion_codigo` | `character(2)` | sí | — | | `estado` | `text` | no | `'borrador'::text` | | `creado_en` | `timestamp with time zone` | no | `now()` | | `actualizado_en` | `timestamp with time zone` | sí | — | | `cuerpo` | `text` | no | `''::text` | | `secciones` | `jsonb` | no | `'[]'::jsonb` | | `presentacion` | `jsonb` | no | `'{}'::jsonb` | Claves y restricciones: - **CHECK album_presentacion_objeto:** `CHECK (jsonb_typeof(presentacion) = 'object'::text)` - **CHECK album_secciones_array:** `CHECK (jsonb_typeof(secciones) = 'array'::text)` - **FK album_estilo_id_estilo_id_fk:** `FOREIGN KEY (estilo_id) REFERENCES estilo(id)` - **FK album_pais_edicion_codigo_pais_codigo_fk:** `FOREIGN KEY (pais_edicion_codigo) REFERENCES pais(codigo)` - **FK album_portada_id_medio_id_fk:** `FOREIGN KEY (portada_id) REFERENCES medio(id) ON DELETE SET NULL` - **PK album_pkey:** `PRIMARY KEY (id)` - **UNIQUE album_slug_unique:** `UNIQUE (slug)` Índices adicionales: `album_estilo`, `album_fecha`. ### `album_enlace` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `album_id` | `text` | no | — | | `plataforma` | `text` | no | — | | `url` | `text` | no | — | Claves y restricciones: - **FK album_enlace_album_id_album_id_fk:** `FOREIGN KEY (album_id) REFERENCES album(id) ON DELETE CASCADE` - **PK album_enlace_album_id_plataforma_pk:** `PRIMARY KEY (album_id, plataforma)` ### `autoria` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `cancion_id` | `text` | no | — | | `persona_id` | `text` | no | — | | `rol` | `text` | no | — | | `porcentaje` | `numeric(5,2)` | no | — | Claves y restricciones: - **FK autoria_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE CASCADE` - **FK autoria_persona_id_persona_id_fk:** `FOREIGN KEY (persona_id) REFERENCES persona(id)` - **PK autoria_cancion_id_persona_id_rol_pk:** `PRIMARY KEY (cancion_id, persona_id, rol)` - **t autoria_suma_cien_trg:** `TRIGGER DEFERRABLE INITIALLY DEFERRED` Disparadores: - `autoria_suma_cien_trg`: `CREATE CONSTRAINT TRIGGER autoria_suma_cien_trg AFTER INSERT OR DELETE OR UPDATE ON autoria DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION autoria_suma_cien()` ### `cancion` Tema principal o versión musical. `version_de_id` apunta a otra canción y separa obra principal de arreglo o grabación. | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ----------------------- | -------------------------- | :--: | ----------------------------------------------------------------------------------------- | | `id` | `text` | no | — | | `slug` | `text` | no | — | | `titulo` | `text` | no | — | | `version_de_id` | `text` | sí | — | | `version_nombre` | `text` | sí | — | | `version_orden` | `integer` | no | `0` | | `estilo_id` | `text` | no | — | | `album_id` | `text` | sí | — | | `numero` | `integer` | sí | — | | `duracion_s` | `integer` | sí | — | | `ano` | `integer` | sí | — | | `letra` | `text` | sí | — | | `idioma` | `text` | no | — | | `inspiracion` | `text` | sí | — | | `tiene_letra` | `boolean` | no | `true` | | `destacada` | `boolean` | no | `false` | | `portada_id` | `text` | sí | — | | `ala_venta` | `boolean` | no | `false` | | `estado` | `text` | no | `'borrador'::text` | | `creado_en` | `timestamp with time zone` | no | `now()` | | `actualizado_en` | `timestamp with time zone` | sí | — | | `es_version` | `boolean` | sí | `(version_de_id IS NOT NULL)` | | `padre_no_es_version` | `boolean` | sí | ` CASE WHEN (version_de_id IS NULL) THEN NULL::boolean ELSE false END` | | `version_nota` | `text` | sí | — | | `codigo` | `text` | no | `('CAN-'::text \|\| lpad((nextval('cancion_codigo_seq'::regclass))::text, 6, '0'::text))` | | `version_codigo` | `text` | sí | — | | `formacion_vocal` | `formacion_vocal` | sí | — | | `acceso` | `acceso_cancion` | no | `'publico'::acceso_cancion` | | `inspiracion_secciones` | `jsonb` | no | `'[]'::jsonb` | | `presentacion` | `jsonb` | no | `'{}'::jsonb` | Claves y restricciones: - **CHECK cancion_estado_conocido:** `CHECK (estado = ANY (ARRAY['borrador'::text, 'publicado'::text, 'retirado'::text]))` - **CHECK cancion_idioma_bcp47:** `CHECK (idioma ~ '^[a-z]{2,8}(-[A-Za-z0-9]{1,8})*$'::text)` - **CHECK cancion_inspiracion_secciones_array:** `CHECK (jsonb_typeof(inspiracion_secciones) = 'array'::text)` - **CHECK cancion_no_es_su_propia_version:** `CHECK (version_de_id IS NULL OR version_de_id <> id)` - **CHECK cancion_presentacion_objeto:** `CHECK (jsonb_typeof(presentacion) = 'object'::text)` - **CHECK cancion_version_codigo_presente:** `CHECK (version_de_id IS NULL AND version_codigo IS NULL OR version_de_id IS NOT NULL AND NULLIF(btrim(version_codigo), ''::text) IS NOT NULL)` - **CHECK cancion_version_no_se_vende:** `CHECK (version_de_id IS NULL OR NOT ala_venta)` - **FK cancion_album_id_album_id_fk:** `FOREIGN KEY (album_id) REFERENCES album(id) ON DELETE SET NULL` - **FK cancion_estilo_id_estilo_id_fk:** `FOREIGN KEY (estilo_id) REFERENCES estilo(id)` - **FK cancion_portada_id_medio_id_fk:** `FOREIGN KEY (portada_id) REFERENCES medio(id) ON DELETE SET NULL` - **FK cancion_version_de_fk:** `FOREIGN KEY (version_de_id) REFERENCES cancion(id) ON DELETE CASCADE` - **FK cancion_version_de_principal:** `FOREIGN KEY (version_de_id, padre_no_es_version) REFERENCES cancion(id, es_version)` - **PK cancion_pkey:** `PRIMARY KEY (id)` - **t cancion_publicable:** `TRIGGER DEFERRABLE INITIALLY DEFERRED` - **UNIQUE cancion_id_es_version_unica:** `UNIQUE (id, es_version)` - **UNIQUE cancion_slug_unique:** `UNIQUE (slug)` Índices adicionales: `cancion_album`, `cancion_codigo_unico`, `cancion_destacada`, `cancion_estilo`, `cancion_version_codigo_unico`, `cancion_version_de`. Disparadores: - `cancion_publicable`: `CREATE CONSTRAINT TRIGGER cancion_publicable AFTER INSERT OR UPDATE ON cancion DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION guardar_invariantes_publicacion()` ### `cancion_audio` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | --------------- | --------------- | :--: | ----------------- | | `cancion_id` | `text` | no | — | | `medio_id` | `text` | no | — | | `uso` | `text` | no | — | | `formato` | `text` | no | — | | `bitrate_kbps` | `integer` | sí | — | | `frecuencia_hz` | `integer` | sí | — | | `bits_muestra` | `integer` | sí | — | | `canales` | `integer` | sí | — | | `orden` | `integer` | no | `0` | Claves y restricciones: - **CHECK cancion_audio_bitrate_valido:** `CHECK (bitrate_kbps IS NULL OR bitrate_kbps > 0)` - **CHECK cancion_audio_bits_validos:** `CHECK (bits_muestra IS NULL OR bits_muestra > 0)` - **CHECK cancion_audio_canales_validos:** `CHECK (canales IS NULL OR canales > 0)` - **CHECK cancion_audio_frecuencia_valida:** `CHECK (frecuencia_hz IS NULL OR frecuencia_hz > 0)` - **CHECK cancion_audio_uso_conocido:** `CHECK (uso = ANY (ARRAY['stream'::text, 'descarga'::text, 'master'::text]))` - **FK cancion_audio_cancion_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE CASCADE` - **FK cancion_audio_medio_fk:** `FOREIGN KEY (medio_id) REFERENCES medio(id) ON DELETE RESTRICT` - **PK cancion_audio_pk:** `PRIMARY KEY (cancion_id, uso, formato)` - **t cancion_audios_publicables:** `TRIGGER DEFERRABLE INITIALLY DEFERRED` Índices adicionales: `cancion_audio_cancion`, `cancion_audio_medio_unico`. Disparadores: - `cancion_audios_publicables`: `CREATE CONSTRAINT TRIGGER cancion_audios_publicables AFTER INSERT OR DELETE OR UPDATE ON cancion_audio DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION guardar_invariantes_publicacion()` ### `cancion_enlace` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `cancion_id` | `text` | no | — | | `plataforma` | `text` | no | — | | `url` | `text` | no | — | Claves y restricciones: - **FK cancion_enlace_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE CASCADE` - **PK cancion_enlace_cancion_id_plataforma_pk:** `PRIMARY KEY (cancion_id, plataforma)` ### `cancion_etiqueta` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------- | --------------- | :--: | ----------------- | | `cancion_id` | `text` | no | — | | `etiqueta_id` | `text` | no | — | | `orden` | `integer` | no | `0` | Claves y restricciones: - **FK cancion_etiqueta_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE CASCADE` - **FK cancion_etiqueta_etiqueta_id_etiqueta_id_fk:** `FOREIGN KEY (etiqueta_id) REFERENCES etiqueta(id) ON DELETE CASCADE` - **PK cancion_etiqueta_cancion_id_etiqueta_id_pk:** `PRIMARY KEY (cancion_id, etiqueta_id)` ### `cancion_interprete` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ----------------- | --------------- | :--: | ------------------- | | `cancion_id` | `text` | no | — | | `artista_id` | `text` | no | — | | `rol` | `text` | no | `'principal'::text` | | `instrumento` | `text` | sí | — | | `acreditado_como` | `text` | sí | — | | `orden` | `integer` | no | `0` | Claves y restricciones: - **CHECK cancion_interprete_rol_conocido:** `CHECK (rol = ANY (ARRAY['principal'::text, 'invitado'::text, 'coros'::text, 'instrumentista'::text]))` - **FK cancion_interprete_artista_fk:** `FOREIGN KEY (artista_id) REFERENCES artista(id)` - **FK cancion_interprete_cancion_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE CASCADE` - **PK cancion_interprete_pk:** `PRIMARY KEY (cancion_id, artista_id, rol)` - **t cancion_creditos_publicables:** `TRIGGER DEFERRABLE INITIALLY DEFERRED` Índices adicionales: `cancion_interprete_artista`. Disparadores: - `cancion_creditos_publicables`: `CREATE CONSTRAINT TRIGGER cancion_creditos_publicables AFTER INSERT OR DELETE OR UPDATE ON cancion_interprete DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION guardar_invariantes_publicacion()` ### `cancion_persona` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `cancion_id` | `text` | no | — | | `persona_id` | `text` | no | — | | `rol` | `text` | no | — | | `orden` | `integer` | no | `0` | Claves y restricciones: - **FK cancion_persona_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE CASCADE` - **FK cancion_persona_persona_id_persona_id_fk:** `FOREIGN KEY (persona_id) REFERENCES persona(id)` - **PK cancion_persona_cancion_id_persona_id_rol_pk:** `PRIMARY KEY (cancion_id, persona_id, rol)` ### `credito_album` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `id` | `text` | no | — | | `album_id` | `text` | no | — | | `persona_id` | `text` | sí | — | | `nombre` | `text` | sí | — | | `rol` | `text` | no | — | | `orden` | `integer` | no | `0` | Claves y restricciones: - **FK credito_album_album_id_album_id_fk:** `FOREIGN KEY (album_id) REFERENCES album(id) ON DELETE CASCADE` - **FK credito_album_persona_id_persona_id_fk:** `FOREIGN KEY (persona_id) REFERENCES persona(id)` - **PK credito_album_pkey:** `PRIMARY KEY (id)` Índices adicionales: `credito_album_album`. ### `estilo` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ---------------- | -------------------------- | :--: | ------------------ | | `id` | `text` | no | — | | `slug` | `text` | no | — | | `nombre` | `text` | no | — | | `resumen` | `text` | no | `''::text` | | `orden` | `integer` | no | `0` | | `principal` | `boolean` | no | `false` | | `imagen_id` | `text` | sí | — | | `estado` | `text` | no | `'borrador'::text` | | `creado_en` | `timestamp with time zone` | no | `now()` | | `actualizado_en` | `timestamp with time zone` | sí | — | | `cuerpo` | `text` | no | `''::text` | Claves y restricciones: - **FK estilo_imagen_id_medio_id_fk:** `FOREIGN KEY (imagen_id) REFERENCES medio(id) ON DELETE SET NULL` - **PK estilo_pkey:** `PRIMARY KEY (id)` - **UNIQUE estilo_slug_unique:** `UNIQUE (slug)` Índices adicionales: `estilo_orden`, `estilo_principal_unico`. ### `etiqueta` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | -------- | --------------- | :--: | ----------------- | | `id` | `text` | no | — | | `slug` | `text` | no | — | | `nombre` | `text` | no | — | Claves y restricciones: - **PK etiqueta_pkey:** `PRIMARY KEY (id)` - **UNIQUE etiqueta_slug_unique:** `UNIQUE (slug)` ### `registro_obra` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `cancion_id` | `text` | no | — | | `iswc` | `text` | sí | — | | `isrc` | `text` | sí | — | | `entidad` | `text` | sí | — | | `numero` | `text` | sí | — | | `editorial` | `text` | sí | — | Claves y restricciones: - **FK registro_obra_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE CASCADE` - **PK registro_obra_pkey:** `PRIMARY KEY (cancion_id)` ### `traduccion` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `cancion_id` | `text` | no | — | | `idioma` | `text` | no | — | | `texto` | `text` | no | — | Claves y restricciones: - **FK traduccion_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE CASCADE` - **PK traduccion_cancion_id_idioma_pk:** `PRIMARY KEY (cancion_id, idioma)` ### `video` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `id` | `text` | no | — | | `cancion_id` | `text` | no | — | | `youtube_id` | `text` | no | — | | `titulo` | `text` | no | — | | `orden` | `integer` | no | `0` | Claves y restricciones: - **FK video_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE CASCADE` - **PK video_pkey:** `PRIMARY KEY (id)` Índices adicionales: `video_cancion`. ## Cuentas y suscripciones ### `codigo_acceso` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ----------- | -------------------------- | :--: | ----------------- | | `id` | `text` | no | — | | `email` | `text` | no | — | | `hash` | `text` | no | — | | `expira_en` | `timestamp with time zone` | no | — | | `intentos` | `integer` | no | `0` | | `usado_en` | `timestamp with time zone` | sí | — | | `creado_en` | `timestamp with time zone` | no | `now()` | Claves y restricciones: - **PK codigo_acceso_pkey:** `PRIMARY KEY (id)` Índices adicionales: `codigo_acceso_email`. ### `identidad` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------------ | -------------------------- | :--: | ----------------- | | `id` | `text` | no | — | | `proveedor` | `text` | no | — | | `sujeto` | `text` | no | — | | `usuario_id` | `text` | no | — | | `email` | `text` | sí | — | | `creada_en` | `timestamp with time zone` | no | `now()` | | `ultimo_acceso_en` | `timestamp with time zone` | sí | — | Claves y restricciones: - **FK identidad_usuario_id_usuario_id_fk:** `FOREIGN KEY (usuario_id) REFERENCES usuario(id) ON DELETE CASCADE` - **PK identidad_pkey:** `PRIMARY KEY (id)` Índices adicionales: `identidad_proveedor_sujeto`, `identidad_usuario`. ### `limite` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ----------- | --------------- | :--: | ----------------- | | `clave` | `text` | no | — | | `intentos` | `integer` | no | — | | `expira_en` | `bigint` | no | — | Claves y restricciones: - **PK limite_pkey:** `PRIMARY KEY (clave)` Índices adicionales: `limite_expira`. ### `plan_suscripcion` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | --------------------------- | -------------------------- | :--: | ------------------ | | `id` | `text` | no | — | | `codigo` | `text` | no | — | | `nombre` | `text` | no | — | | `descripcion` | `text` | sí | — | | `nivel` | `integer` | no | — | | `precio_centimos` | `integer` | no | — | | `moneda` | `text` | no | `'eur'::text` | | `periodicidad` | `text` | no | — | | `intervalos` | `integer` | no | `1` | | `referencia_precio_externa` | `text` | sí | — | | `prestaciones` | `jsonb` | no | `'[]'::jsonb` | | `estado` | `text` | no | `'borrador'::text` | | `creado_en` | `timestamp with time zone` | no | `now()` | | `actualizado_en` | `timestamp with time zone` | no | `now()` | | `incluye_publicidad` | `boolean` | no | `false` | | `calidad_escucha` | `text` | sí | `'media'::text` | | `descargas_incluidas` | `integer` | no | `0` | | `calidad_descarga` | `text` | sí | — | | `acceso_versiones` | `boolean` | no | `false` | | `ambito` | `text` | no | `'musica'::text` | | `acceso_contenido_taller` | `boolean` | no | `false` | | `descarga_material_taller` | `boolean` | no | `false` | | `acceso_jukebox` | `boolean` | no | `false` | Claves y restricciones: - **CHECK plan_suscripcion_ambito_conocido:** `CHECK (ambito = ANY (ARRAY['musica'::text, 'taller_letras'::text]))` - **CHECK plan_suscripcion_calidad_descarga_conocida:** `CHECK (calidad_descarga IS NULL OR (calidad_descarga = ANY (ARRAY['media'::text, 'alta'::text])))` - **CHECK plan_suscripcion_calidad_escucha_conocida:** `CHECK (calidad_escucha IS NULL OR (calidad_escucha = ANY (ARRAY['media'::text, 'alta'::text])))` - **CHECK plan_suscripcion_calidad_solo_con_descargas:** `CHECK (descargas_incluidas > 0 OR calidad_descarga IS NULL)` - **CHECK plan_suscripcion_capacidades_segun_ambito:** `CHECK (ambito = 'musica'::text AND calidad_escucha IS NOT NULL AND NOT acceso_contenido_taller AND NOT descarga_material_taller OR ambito = 'taller_letras'::text AND calidad_escucha IS NULL AND NOT incluye_publicidad AND descargas_incluidas = 0 AND calidad_descarga IS NULL AND NOT acceso_versiones AND NOT acceso_jukebox)` - **CHECK plan_suscripcion_codigo_no_vacio:** `CHECK (btrim(codigo) <> ''::text)` - **CHECK plan_suscripcion_descargas_no_negativas:** `CHECK (descargas_incluidas >= 0)` - **CHECK plan_suscripcion_estado_conocido:** `CHECK (estado = ANY (ARRAY['borrador'::text, 'disponible'::text, 'retirado'::text]))` - **CHECK plan_suscripcion_intervalos_positivos:** `CHECK (intervalos > 0)` - **CHECK plan_suscripcion_moneda_iso:** `CHECK (moneda ~ '^[a-z]{3}$'::text)` - **CHECK plan_suscripcion_nivel_positivo:** `CHECK (nivel > 0)` - **CHECK plan_suscripcion_nombre_no_vacio:** `CHECK (btrim(nombre) <> ''::text)` - **CHECK plan_suscripcion_periodicidad_conocida:** `CHECK (periodicidad = ANY (ARRAY['dia'::text, 'semana'::text, 'mes'::text, 'ano'::text]))` - **CHECK plan_suscripcion_precio_no_negativo:** `CHECK (precio_centimos >= 0)` - **CHECK plan_suscripcion_prestaciones_array:** `CHECK (jsonb_typeof(prestaciones) = 'array'::text)` - **PK plan_suscripcion_pkey:** `PRIMARY KEY (id)` - **UNIQUE plan_suscripcion_codigo_unique:** `UNIQUE (codigo)` - **UNIQUE plan_suscripcion_referencia_precio_externa_unique:** `UNIQUE (referencia_precio_externa)` Índices adicionales: `plan_suscripcion_estado_nivel`. ### `sesion` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | -------------------------- | :--: | ----------------- | | `id` | `text` | no | — | | `usuario_id` | `text` | no | — | | `expira_en` | `timestamp with time zone` | no | — | | `creada_en` | `timestamp with time zone` | no | `now()` | Claves y restricciones: - **FK sesion_usuario_id_usuario_id_fk:** `FOREIGN KEY (usuario_id) REFERENCES usuario(id) ON DELETE CASCADE` - **PK sesion_pkey:** `PRIMARY KEY (id)` Índices adicionales: `sesion_usuario`. ### `suscripcion` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | -------------------- | -------------------------- | :--: | ------------------- | | `id` | `text` | no | — | | `usuario_id` | `text` | no | — | | `estado` | `text` | no | `'pendiente'::text` | | `inicia_en` | `timestamp with time zone` | no | `now()` | | `termina_en` | `timestamp with time zone` | sí | — | | `proveedor` | `text` | sí | — | | `referencia_externa` | `text` | sí | — | | `creada_en` | `timestamp with time zone` | no | `now()` | | `actualizada_en` | `timestamp with time zone` | no | `now()` | | `plan_id` | `text` | sí | — | Claves y restricciones: - **CHECK suscripcion_estado_conocido:** `CHECK (estado = ANY (ARRAY['pendiente'::text, 'activa'::text, 'pausada'::text, 'cancelada'::text, 'vencida'::text]))` - **CHECK suscripcion_periodo_valido:** `CHECK (termina_en IS NULL OR termina_en > inicia_en)` - **CHECK suscripcion_referencia_completa:** `CHECK ((proveedor IS NULL) = (referencia_externa IS NULL))` - **FK suscripcion_plan_id_plan_suscripcion_id_fk:** `FOREIGN KEY (plan_id) REFERENCES plan_suscripcion(id) ON DELETE RESTRICT` - **FK suscripcion_usuario_id_usuario_id_fk:** `FOREIGN KEY (usuario_id) REFERENCES usuario(id) ON DELETE CASCADE` - **PK suscripcion_pkey:** `PRIMARY KEY (id)` Índices adicionales: `suscripcion_proveedor_referencia`, `suscripcion_usuario_estado`. ### `usuario` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------------ | -------------------------- | :--: | ----------------- | | `id` | `text` | no | — | | `email` | `text` | no | — | | `nombre` | `text` | sí | — | | `creado_en` | `timestamp with time zone` | no | `now()` | | `ultimo_acceso_en` | `timestamp with time zone` | sí | — | | `rol` | `text` | no | `'persona'::text` | Claves y restricciones: - **CHECK usuario_rol_conocido:** `CHECK (rol = ANY (ARRAY['persona'::text, 'admin'::text]))` - **PK usuario_pkey:** `PRIMARY KEY (id)` - **UNIQUE usuario_email_unique:** `UNIQUE (email)` ## Interacción y escucha ### `acceso_cancion_usuario` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | -------------- | -------------------------- | :--: | ----------------- | | `usuario_id` | `text` | no | — | | `cancion_id` | `text` | no | — | | `concedido_en` | `timestamp with time zone` | no | `now()` | Claves y restricciones: - **FK acceso_cancion_usuario_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE CASCADE` - **FK acceso_cancion_usuario_usuario_id_usuario_id_fk:** `FOREIGN KEY (usuario_id) REFERENCES usuario(id) ON DELETE CASCADE` - **PK acceso_cancion_usuario_usuario_id_cancion_id_pk:** `PRIMARY KEY (usuario_id, cancion_id)` Índices adicionales: `acceso_cancion_usuario_cancion`. ### `favorito` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | -------------------------- | :--: | ----------------- | | `usuario_id` | `text` | no | — | | `marcado_en` | `timestamp with time zone` | no | `now()` | | `cancion_id` | `text` | no | — | Claves y restricciones: - **FK favorito_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE CASCADE` - **FK favorito_usuario_id_usuario_id_fk:** `FOREIGN KEY (usuario_id) REFERENCES usuario(id) ON DELETE CASCADE` - **PK favorito_usuario_id_cancion_id_pk:** `PRIMARY KEY (usuario_id, cancion_id)` Índices adicionales: `idx_favorito_usuario`. ### `lista_reproduccion` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ---------------- | -------------------------- | :--: | ----------------- | | `id` | `text` | no | — | | `usuario_id` | `text` | no | — | | `nombre` | `text` | no | — | | `creada_en` | `timestamp with time zone` | no | `now()` | | `actualizada_en` | `timestamp with time zone` | no | `now()` | Claves y restricciones: - **CHECK lista_reproduccion_nombre_valido:** `CHECK (NULLIF(btrim(nombre), ''::text) IS NOT NULL AND char_length(btrim(nombre)) <= 80)` - **FK lista_reproduccion_usuario_id_usuario_id_fk:** `FOREIGN KEY (usuario_id) REFERENCES usuario(id) ON DELETE CASCADE` - **PK lista_reproduccion_pkey:** `PRIMARY KEY (id)` Índices adicionales: `lista_reproduccion_nombre_usuario`, `lista_reproduccion_usuario`. ### `lista_reproduccion_cancion` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | -------------------------- | :--: | ----------------- | | `lista_id` | `text` | no | — | | `cancion_id` | `text` | no | — | | `orden` | `integer` | no | — | | `anadida_en` | `timestamp with time zone` | no | `now()` | Claves y restricciones: - **CHECK lista_reproduccion_orden_positivo:** `CHECK (orden >= 0)` - **FK lista_reproduccion_cancion_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE CASCADE` - **FK lista_reproduccion_cancion_lista_id_lista_reproduccion_id_fk:** `FOREIGN KEY (lista_id) REFERENCES lista_reproduccion(id) ON DELETE CASCADE` - **PK lista_reproduccion_cancion_lista_id_cancion_id_pk:** `PRIMARY KEY (lista_id, cancion_id)` Índices adicionales: `lista_reproduccion_cancion_tema`, `lista_reproduccion_orden_unico`. ### `reproduccion` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | -------------------------- | :--: | ----------------- | | `id` | `text` | no | — | | `usuario_id` | `text` | sí | — | | `segundos` | `integer` | no | `0` | | `dia` | `text` | no | — | | `creado_en` | `timestamp with time zone` | no | `now()` | | `cancion_id` | `text` | no | — | Claves y restricciones: - **FK reproduccion_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE CASCADE` - **FK reproduccion_usuario_id_usuario_id_fk:** `FOREIGN KEY (usuario_id) REFERENCES usuario(id) ON DELETE SET NULL` - **PK reproduccion_pkey:** `PRIMARY KEY (id)` Índices adicionales: `reproduccion_cancion`, `reproduccion_dia`, `reproduccion_usuario`. ### `valoracion` Interacción unificada por cuenta y canción. `like` alimenta el contador público; `value` conserva la puntuación de 1 a 5 para un uso profesional posterior. Son independientes. | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ---------------- | -------------------------- | :--: | ----------------- | | `usuario_id` | `text` | no | — | | `value` | `integer` | sí | — | | `date` | `timestamp with time zone` | no | `now()` | | `actualizada_en` | `timestamp with time zone` | no | `now()` | | `cancion_id` | `text` | no | — | | `like` | `boolean` | no | `false` | Claves y restricciones: - **CHECK valoracion_de_uno_a_cinco:** `CHECK (value >= 1 AND value <= 5)` - **FK valoracion_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE CASCADE` - **FK valoracion_usuario_id_usuario_id_fk:** `FOREIGN KEY (usuario_id) REFERENCES usuario(id) ON DELETE CASCADE` - **PK valoracion_usuario_id_cancion_id_pk:** `PRIMARY KEY (usuario_id, cancion_id)` Índices adicionales: `idx_valoracion_cancion`. ## Comercio ### `carrito` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ---------------- | -------------------------- | :--: | ----------------- | | `id` | `text` | no | — | | `usuario_id` | `text` | sí | — | | `creado_en` | `timestamp with time zone` | no | `now()` | | `actualizado_en` | `timestamp with time zone` | no | `now()` | Claves y restricciones: - **FK carrito_usuario_id_usuario_id_fk:** `FOREIGN KEY (usuario_id) REFERENCES usuario(id) ON DELETE CASCADE` - **PK carrito_pkey:** `PRIMARY KEY (id)` Índices adicionales: `carrito_usuario_unico`. ### `carrito_item` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | -------------------------- | :--: | ----------------- | | `carrito_id` | `text` | no | — | | `anadido_en` | `timestamp with time zone` | no | `now()` | | `cancion_id` | `text` | no | — | Claves y restricciones: - **FK carrito_item_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE CASCADE` - **FK carrito_item_carrito_id_carrito_id_fk:** `FOREIGN KEY (carrito_id) REFERENCES carrito(id) ON DELETE CASCADE` - **PK carrito_item_carrito_id_cancion_id_pk:** `PRIMARY KEY (carrito_id, cancion_id)` ### `carrito_producto` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------- | -------------------------- | :--: | ----------------- | | `carrito_id` | `text` | no | — | | `producto_id` | `text` | no | — | | `cantidad` | `integer` | no | `1` | | `anadido_en` | `timestamp with time zone` | no | `now()` | Claves y restricciones: - **CHECK carrito_producto_cantidad_positiva:** `CHECK (cantidad > 0)` - **FK carrito_producto_carrito_id_carrito_id_fk:** `FOREIGN KEY (carrito_id) REFERENCES carrito(id) ON DELETE CASCADE` - **FK carrito_producto_producto_id_producto_id_fk:** `FOREIGN KEY (producto_id) REFERENCES producto(id) ON DELETE CASCADE` - **PK carrito_producto_carrito_id_producto_id_pk:** `PRIMARY KEY (carrito_id, producto_id)` ### `compra` Permiso materializado de descarga de una canción adquirida. | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------- | -------------------------- | :--: | ----------------- | | `usuario_id` | `text` | no | — | | `pedido_id` | `text` | no | — | | `comprado_en` | `timestamp with time zone` | no | `now()` | | `cancion_id` | `text` | no | — | Claves y restricciones: - **FK compra_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE RESTRICT` - **FK compra_pedido_id_pedido_id_fk:** `FOREIGN KEY (pedido_id) REFERENCES pedido(id) ON DELETE CASCADE` - **FK compra_usuario_id_usuario_id_fk:** `FOREIGN KEY (usuario_id) REFERENCES usuario(id) ON DELETE CASCADE` - **PK compra_usuario_id_cancion_id_pk:** `PRIMARY KEY (usuario_id, cancion_id)` ### `intencion_pago` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ---------------- | -------------------------- | :--: | ----------------- | | `id` | `text` | no | — | | `usuario_id` | `text` | no | — | | `huella` | `text` | no | — | | `datos` | `jsonb` | no | — | | `sesion_pago_id` | `text` | sí | — | | `estado` | `text` | no | `'abierta'::text` | | `creado_en` | `timestamp with time zone` | no | `now()` | Claves y restricciones: - **FK intencion_pago_usuario_id_usuario_id_fk:** `FOREIGN KEY (usuario_id) REFERENCES usuario(id) ON DELETE CASCADE` - **PK intencion_pago_pkey:** `PRIMARY KEY (id)` - **UNIQUE intencion_pago_sesion_pago_id_unique:** `UNIQUE (sesion_pago_id)` Índices adicionales: `intencion_pago_abierta`. ### `pedido` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ---------------- | -------------------------- | :--: | ------------------- | | `id` | `text` | no | — | | `usuario_id` | `text` | no | — | | `sesion_pago_id` | `text` | no | — | | `estado` | `text` | no | `'pendiente'::text` | | `packs` | `integer` | no | — | | `total_centimos` | `integer` | no | — | | `moneda` | `text` | no | `'eur'::text` | | `creado_en` | `timestamp with time zone` | no | `now()` | | `pagado_en` | `timestamp with time zone` | sí | — | Claves y restricciones: - **FK pedido_usuario_id_usuario_id_fk:** `FOREIGN KEY (usuario_id) REFERENCES usuario(id) ON DELETE CASCADE` - **PK pedido_pkey:** `PRIMARY KEY (id)` - **UNIQUE pedido_sesion_pago_id_unique:** `UNIQUE (sesion_pago_id)` Índices adicionales: `pedido_usuario`. ### `pedido_item` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `pedido_id` | `text` | no | — | | `titulo` | `text` | no | — | | `cancion_id` | `text` | no | — | Claves y restricciones: - **FK pedido_item_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE RESTRICT` - **FK pedido_item_pedido_id_pedido_id_fk:** `FOREIGN KEY (pedido_id) REFERENCES pedido(id) ON DELETE CASCADE` - **PK pedido_item_pedido_id_cancion_id_pk:** `PRIMARY KEY (pedido_id, cancion_id)` ### `pedido_linea` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | -------------------------- | --------------- | :--: | ----------------- | | `pedido_id` | `text` | no | — | | `codigo` | `text` | no | — | | `producto_id` | `text` | sí | — | | `tipo` | `text` | no | — | | `nombre` | `text` | no | — | | `descripcion` | `text` | sí | — | | `cantidad` | `integer` | no | — | | `precio_unitario_centimos` | `integer` | no | — | | `total_centimos` | `integer` | no | — | | `moneda` | `text` | no | — | | `modalidad` | `text` | no | — | | `datos` | `jsonb` | no | `'{}'::jsonb` | Claves y restricciones: - **CHECK pedido_linea_cantidad_positiva:** `CHECK (cantidad > 0)` - **CHECK pedido_linea_datos_objeto:** `CHECK (jsonb_typeof(datos) = 'object'::text)` - **CHECK pedido_linea_importes_validos:** `CHECK (precio_unitario_centimos >= 0 AND total_centimos = (precio_unitario_centimos * cantidad))` - **CHECK pedido_linea_modalidad_conocida:** `CHECK (modalidad = ANY (ARRAY['pago_unico'::text, 'suscripcion'::text]))` - **CHECK pedido_linea_moneda_iso:** `CHECK (moneda ~ '^[a-z]{3}$'::text)` - **CHECK pedido_linea_tipo_conocido:** `CHECK (tipo = ANY (ARRAY['pack_canciones'::text, 'libro'::text, 'partitura'::text, 'midi'::text, 'stems'::text, 'servicio'::text, 'suscripcion'::text]))` - **FK pedido_linea_pedido_id_pedido_id_fk:** `FOREIGN KEY (pedido_id) REFERENCES pedido(id) ON DELETE CASCADE` - **FK pedido_linea_producto_id_producto_id_fk:** `FOREIGN KEY (producto_id) REFERENCES producto(id) ON DELETE SET NULL` - **PK pedido_linea_pedido_id_codigo_pk:** `PRIMARY KEY (pedido_id, codigo)` Índices adicionales: `pedido_linea_tipo`. ### `producto` Oferta comercial genérica; sus datos específicos se conservan en JSON. | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ----------------- | -------------------------- | :--: | ------------------ | | `id` | `text` | no | — | | `codigo` | `text` | no | — | | `tipo` | `text` | no | — | | `nombre` | `text` | no | — | | `descripcion` | `text` | sí | — | | `estado` | `text` | no | `'borrador'::text` | | `precio_centimos` | `integer` | no | — | | `moneda` | `text` | no | `'eur'::text` | | `metadatos` | `jsonb` | no | `'{}'::jsonb` | | `creado_en` | `timestamp with time zone` | no | `now()` | | `actualizado_en` | `timestamp with time zone` | no | `now()` | Claves y restricciones: - **CHECK producto_codigo_no_vacio:** `CHECK (btrim(codigo) <> ''::text)` - **CHECK producto_estado_conocido:** `CHECK (estado = ANY (ARRAY['borrador'::text, 'disponible'::text, 'retirado'::text]))` - **CHECK producto_metadatos_objeto:** `CHECK (jsonb_typeof(metadatos) = 'object'::text)` - **CHECK producto_moneda_iso:** `CHECK (moneda ~ '^[a-z]{3}$'::text)` - **CHECK producto_nombre_no_vacio:** `CHECK (btrim(nombre) <> ''::text)` - **CHECK producto_precio_no_negativo:** `CHECK (precio_centimos >= 0)` - **CHECK producto_tipo_conocido:** `CHECK (tipo = ANY (ARRAY['libro'::text, 'partitura'::text, 'midi'::text, 'stems'::text, 'servicio'::text]))` - **PK producto_pkey:** `PRIMARY KEY (id)` - **UNIQUE producto_codigo_unique:** `UNIQUE (codigo)` Índices adicionales: `producto_tipo_estado`. ## Facturación ### `contador_factura` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ---------------- | -------------------------- | :--: | ----------------- | | `serie` | `text` | no | — | | `ultimo_numero` | `integer` | no | `0` | | `actualizado_en` | `timestamp with time zone` | no | `now()` | Claves y restricciones: - **PK contador_factura_pkey:** `PRIMARY KEY (serie)` ### `datos_facturacion` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ---------------- | -------------------------- | :--: | -------------------- | | `id` | `text` | no | — | | `usuario_id` | `text` | no | — | | `tipo` | `text` | no | `'particular'::text` | | `nombre` | `text` | no | — | | `nif` | `text` | sí | — | | `direccion` | `text` | sí | — | | `cp` | `text` | sí | — | | `ciudad` | `text` | sí | — | | `provincia` | `text` | sí | — | | `pais_codigo` | `character(2)` | no | — | | `predeterminada` | `boolean` | no | `false` | | `creado_en` | `timestamp with time zone` | no | `now()` | | `actualizado_en` | `timestamp with time zone` | sí | — | Claves y restricciones: - **FK datos_facturacion_pais_codigo_pais_codigo_fk:** `FOREIGN KEY (pais_codigo) REFERENCES pais(codigo)` - **FK datos_facturacion_usuario_id_usuario_id_fk:** `FOREIGN KEY (usuario_id) REFERENCES usuario(id) ON DELETE CASCADE` - **PK datos_facturacion_pkey:** `PRIMARY KEY (id)` Índices adicionales: `datos_facturacion_usuario`. ### `descarga` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | -------------------------- | :--: | ----------------- | | `id` | `text` | no | — | | `usuario_id` | `text` | no | — | | `bajada_en` | `timestamp with time zone` | no | `now()` | | `ip` | `text` | sí | — | | `agente` | `text` | sí | — | | `cancion_id` | `text` | no | — | Claves y restricciones: - **FK descarga_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE RESTRICT` - **FK descarga_usuario_id_usuario_id_fk:** `FOREIGN KEY (usuario_id) REFERENCES usuario(id) ON DELETE CASCADE` - **PK descarga_pkey:** `PRIMARY KEY (id)` Índices adicionales: `descarga_fecha`, `descarga_usuario`. ### `factura` Documento fiscal inmutable; una rectificativa referencia otra factura. | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | -------------------- | -------------------------- | :--: | ----------------- | | `id` | `text` | no | — | | `pedido_id` | `text` | no | — | | `serie` | `text` | no | — | | `numero` | `integer` | no | — | | `tipo` | `text` | no | `'normal'::text` | | `rectifica_a_id` | `text` | sí | — | | `fecha` | `timestamp with time zone` | no | `now()` | | `base_centimos` | `integer` | no | — | | `iva_tipo` | `integer` | no | — | | `iva_cuota_centimos` | `integer` | no | — | | `total_centimos` | `integer` | no | — | | `moneda` | `text` | no | `'eur'::text` | | `datos_congelados` | `jsonb` | no | — | | `pais_codigo` | `character(2)` | no | — | Claves y restricciones: - **CHECK factura_rectificativa_apunta:** `CHECK ((tipo = 'rectificativa'::text) = (rectifica_a_id IS NOT NULL))` - **FK factura_pais_codigo_pais_codigo_fk:** `FOREIGN KEY (pais_codigo) REFERENCES pais(codigo)` - **FK factura_pedido_id_pedido_id_fk:** `FOREIGN KEY (pedido_id) REFERENCES pedido(id)` - **FK factura_rectifica_fk:** `FOREIGN KEY (rectifica_a_id) REFERENCES factura(id)` - **PK factura_pkey:** `PRIMARY KEY (id)` Índices adicionales: `factura_fecha`, `factura_pedido`, `factura_serie_numero`. ## Editorial y legal ### `documento_legal` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | -------------------- | --------------- | :--: | ----------------- | | `id` | `text` | no | — | | `slug` | `text` | no | — | | `titulo` | `text` | no | — | | `resumen` | `text` | no | `''::text` | | `orden` | `integer` | no | `0` | | `version_vigente_id` | `text` | sí | — | Claves y restricciones: - **FK documento_legal_version_vigente_fk:** `FOREIGN KEY (version_vigente_id) REFERENCES version_legal(id) ON DELETE SET NULL` - **PK documento_legal_pkey:** `PRIMARY KEY (id)` - **UNIQUE documento_legal_slug_unique:** `UNIQUE (slug)` ### `entrada` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ---------------- | -------------------------- | :--: | ------------------ | | `id` | `text` | no | — | | `slug` | `text` | no | — | | `titulo` | `text` | no | — | | `fecha` | `text` | no | — | | `resumen` | `text` | no | `''::text` | | `cuerpo` | `text` | no | `''::text` | | `imagen_id` | `text` | sí | — | | `estado` | `text` | no | `'borrador'::text` | | `creado_en` | `timestamp with time zone` | no | `now()` | | `actualizado_en` | `timestamp with time zone` | sí | — | | `secciones` | `jsonb` | no | `'[]'::jsonb` | | `presentacion` | `jsonb` | no | `'{}'::jsonb` | Claves y restricciones: - **CHECK entrada_presentacion_objeto:** `CHECK (jsonb_typeof(presentacion) = 'object'::text)` - **CHECK entrada_secciones_array:** `CHECK (jsonb_typeof(secciones) = 'array'::text)` - **FK entrada_imagen_id_medio_id_fk:** `FOREIGN KEY (imagen_id) REFERENCES medio(id) ON DELETE SET NULL` - **PK entrada_pkey:** `PRIMARY KEY (id)` - **UNIQUE entrada_slug_unique:** `UNIQUE (slug)` Índices adicionales: `entrada_fecha`. ### `entrada_cancion` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `entrada_id` | `text` | no | — | | `cancion_id` | `text` | no | — | Claves y restricciones: - **FK entrada_cancion_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE CASCADE` - **FK entrada_cancion_entrada_id_entrada_id_fk:** `FOREIGN KEY (entrada_id) REFERENCES entrada(id) ON DELETE CASCADE` - **PK entrada_cancion_entrada_id_cancion_id_pk:** `PRIMARY KEY (entrada_id, cancion_id)` ### `entrada_etiqueta` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------- | --------------- | :--: | ----------------- | | `entrada_id` | `text` | no | — | | `etiqueta_id` | `text` | no | — | Claves y restricciones: - **FK entrada_etiqueta_entrada_id_entrada_id_fk:** `FOREIGN KEY (entrada_id) REFERENCES entrada(id) ON DELETE CASCADE` - **FK entrada_etiqueta_etiqueta_id_etiqueta_id_fk:** `FOREIGN KEY (etiqueta_id) REFERENCES etiqueta(id) ON DELETE CASCADE` - **PK entrada_etiqueta_entrada_id_etiqueta_id_pk:** `PRIMARY KEY (entrada_id, etiqueta_id)` ### `pagina` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ---------------- | -------------------------- | :--: | ------------------ | | `id` | `text` | no | — | | `slug` | `text` | no | — | | `titulo` | `text` | no | — | | `descripcion` | `text` | no | `''::text` | | `cuerpo` | `text` | no | `''::text` | | `estado` | `text` | no | `'borrador'::text` | | `creado_en` | `timestamp with time zone` | no | `now()` | | `actualizado_en` | `timestamp with time zone` | sí | — | Claves y restricciones: - **PK pagina_pkey:** `PRIMARY KEY (id)` - **UNIQUE pagina_slug_unique:** `UNIQUE (slug)` ### `redireccion` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | -------------------------- | :--: | ----------------- | | `desde` | `text` | no | — | | `hacia` | `text` | no | — | | `permanente` | `boolean` | no | `true` | | `motivo` | `text` | sí | — | | `creada_en` | `timestamp with time zone` | no | `now()` | Claves y restricciones: - **PK redireccion_pkey:** `PRIMARY KEY (desde)` Índices adicionales: `redireccion_hacia`. ### `version_legal` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ----------------- | -------------------------- | :--: | ----------------- | | `id` | `text` | no | — | | `documento_id` | `text` | no | — | | `cuerpo` | `text` | no | — | | `vigente_desde` | `timestamp with time zone` | no | `now()` | | `resumen_cambios` | `text` | sí | — | Claves y restricciones: - **FK version_legal_documento_id_documento_legal_id_fk:** `FOREIGN KEY (documento_id) REFERENCES documento_legal(id) ON DELETE CASCADE` - **PK version_legal_pkey:** `PRIMARY KEY (id)` Índices adicionales: `version_legal_documento`. ## Taller de letras ### `acepcion` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `id` | `text` | no | — | | `termino_id` | `text` | no | — | | `numero` | `integer` | no | — | | `categoria` | `text` | no | — | | `definicion` | `text` | no | — | | `ampliacion` | `text` | sí | — | | `ejemplo` | `text` | sí | — | Claves y restricciones: - **FK acepcion_termino_id_termino_id_fk:** `FOREIGN KEY (termino_id) REFERENCES termino(id) ON DELETE CASCADE` - **PK acepcion_pkey:** `PRIMARY KEY (id)` Índices adicionales: `acepcion_numero`. ### `animo` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------- | --------------- | :--: | ----------------- | | `id` | `text` | no | — | | `codigo` | `text` | no | — | | `nombre` | `text` | no | — | | `gems` | `text` | sí | — | | `valencia` | `integer` | sí | — | | `energia` | `integer` | sí | — | | `descripcion` | `text` | sí | — | Claves y restricciones: - **PK animo_pkey:** `PRIMARY KEY (id)` - **UNIQUE animo_codigo_unique:** `UNIQUE (codigo)` ### `cancion_genero` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `cancion_id` | `text` | no | — | | `genero_id` | `text` | no | — | Claves y restricciones: - **FK cancion_genero_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE CASCADE` - **FK cancion_genero_genero_id_genero_id_fk:** `FOREIGN KEY (genero_id) REFERENCES genero(id) ON DELETE CASCADE` - **PK cancion_genero_cancion_id_genero_id_pk:** `PRIMARY KEY (cancion_id, genero_id)` ### `capitulo` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ---------------- | -------------------------- | :--: | ------------------ | | `id` | `text` | no | — | | `slug` | `text` | no | — | | `titulo` | `text` | no | — | | `tipo` | `text` | no | — | | `genero_id` | `text` | sí | — | | `orden` | `integer` | no | `0` | | `resumen` | `text` | no | `''::text` | | `cuerpo` | `text` | no | `''::text` | | `estado` | `text` | no | `'borrador'::text` | | `creado_en` | `timestamp with time zone` | no | `now()` | | `actualizado_en` | `timestamp with time zone` | sí | — | Claves y restricciones: - **FK capitulo_genero_id_genero_id_fk:** `FOREIGN KEY (genero_id) REFERENCES genero(id) ON DELETE SET NULL` - **PK capitulo_pkey:** `PRIMARY KEY (id)` - **UNIQUE capitulo_slug_unique:** `UNIQUE (slug)` Índices adicionales: `capitulo_tipo`. ### `capitulo_acepcion` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------- | --------------- | :--: | ----------------- | | `capitulo_id` | `text` | no | — | | `acepcion_id` | `text` | no | — | | `automatico` | `boolean` | no | `true` | | `confirmado` | `boolean` | no | `false` | Claves y restricciones: - **FK capitulo_acepcion_acepcion_id_acepcion_id_fk:** `FOREIGN KEY (acepcion_id) REFERENCES acepcion(id) ON DELETE CASCADE` - **FK capitulo_acepcion_capitulo_id_capitulo_id_fk:** `FOREIGN KEY (capitulo_id) REFERENCES capitulo(id) ON DELETE CASCADE` - **PK capitulo_acepcion_capitulo_id_acepcion_id_pk:** `PRIMARY KEY (capitulo_id, acepcion_id)` ### `compas` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------- | --------------- | :--: | ----------------- | | `id` | `text` | no | — | | `codigo` | `text` | no | — | | `nombre` | `text` | sí | — | | `numerador` | `integer` | no | — | | `denominador` | `integer` | no | — | | `nota` | `text` | sí | — | Claves y restricciones: - **PK compas_pkey:** `PRIMARY KEY (id)` - **UNIQUE compas_codigo_unique:** `UNIQUE (codigo)` ### `genero` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ---------------- | -------------------------- | :--: | ------------------ | | `id` | `text` | no | — | | `slug` | `text` | no | — | | `nombre` | `text` | no | — | | `resumen` | `text` | no | `''::text` | | `origen_lugar` | `text` | sí | — | | `origen_ano` | `integer` | sí | — | | `tempo` | `text` | sí | — | | `rima` | `text` | sí | — | | `metrica_nota` | `text` | sí | — | | `compas_nota` | `text` | sí | — | | `rasgos` | `text[]` | sí | — | | `estado` | `text` | no | `'borrador'::text` | | `origen` | `text` | no | `'manual'::text` | | `modelo_origen` | `text` | sí | — | | `importado_en` | `timestamp with time zone` | sí | — | | `revisado_por` | `text` | sí | — | | `revisado_en` | `timestamp with time zone` | sí | — | | `creado_en` | `timestamp with time zone` | no | `now()` | | `actualizado_en` | `timestamp with time zone` | sí | — | Claves y restricciones: - **PK genero_pkey:** `PRIMARY KEY (id)` - **UNIQUE genero_slug_unique:** `UNIQUE (slug)` Índices adicionales: `genero_nombre`, `genero_origen_ano`. ### `genero_alias` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------- | --------------- | :--: | ----------------- | | `id` | `text` | no | — | | `genero_id` | `text` | no | — | | `nombre` | `text` | no | — | | `pais_codigo` | `character(2)` | sí | — | | `nota` | `text` | sí | — | Claves y restricciones: - **FK genero_alias_genero_id_genero_id_fk:** `FOREIGN KEY (genero_id) REFERENCES genero(id) ON DELETE CASCADE` - **FK genero_alias_pais_codigo_pais_codigo_fk:** `FOREIGN KEY (pais_codigo) REFERENCES pais(codigo)` - **PK genero_alias_pkey:** `PRIMARY KEY (id)` Índices adicionales: `genero_alias_nombre`. ### `genero_compas` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ----------- | --------------- | :--: | ----------------- | | `genero_id` | `text` | no | — | | `compas_id` | `text` | no | — | | `principal` | `boolean` | no | `false` | | `nota` | `text` | sí | — | Claves y restricciones: - **FK genero_compas_compas_id_compas_id_fk:** `FOREIGN KEY (compas_id) REFERENCES compas(id)` - **FK genero_compas_genero_id_genero_id_fk:** `FOREIGN KEY (genero_id) REFERENCES genero(id) ON DELETE CASCADE` - **PK genero_compas_genero_id_compas_id_pk:** `PRIMARY KEY (genero_id, compas_id)` ### `genero_instrumento` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ---------------- | --------------- | :--: | ------------------ | | `genero_id` | `text` | no | — | | `instrumento_id` | `text` | no | — | | `importancia` | `text` | no | `'habitual'::text` | | `papel` | `text` | sí | — | | `orden` | `integer` | no | `0` | Claves y restricciones: - **FK genero_instrumento_genero_id_genero_id_fk:** `FOREIGN KEY (genero_id) REFERENCES genero(id) ON DELETE CASCADE` - **FK genero_instrumento_instrumento_id_instrumento_id_fk:** `FOREIGN KEY (instrumento_id) REFERENCES instrumento(id)` - **PK genero_instrumento_genero_id_instrumento_id_pk:** `PRIMARY KEY (genero_id, instrumento_id)` ### `genero_metro` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | -------------- | --------------- | :--: | ----------------- | | `genero_id` | `text` | no | — | | `metro_id` | `text` | no | — | | `predominante` | `boolean` | no | `false` | Claves y restricciones: - **FK genero_metro_genero_id_genero_id_fk:** `FOREIGN KEY (genero_id) REFERENCES genero(id) ON DELETE CASCADE` - **FK genero_metro_metro_id_metro_id_fk:** `FOREIGN KEY (metro_id) REFERENCES metro(id)` - **PK genero_metro_genero_id_metro_id_pk:** `PRIMARY KEY (genero_id, metro_id)` ### `genero_origen` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ----------- | --------------- | :--: | ----------------- | | `genero_id` | `text` | no | — | | `origen_id` | `text` | no | — | | `tipo` | `text` | no | — | | `principal` | `boolean` | no | `false` | | `nota` | `text` | sí | — | | `orden` | `integer` | no | `0` | Claves y restricciones: - **CHECK genero_origen_no_reflexivo:** `CHECK (genero_id <> origen_id)` - **FK genero_origen_genero_id_genero_id_fk:** `FOREIGN KEY (genero_id) REFERENCES genero(id) ON DELETE CASCADE` - **FK genero_origen_origen_id_genero_id_fk:** `FOREIGN KEY (origen_id) REFERENCES genero(id) ON DELETE CASCADE` - **PK genero_origen_genero_id_origen_id_tipo_pk:** `PRIMARY KEY (genero_id, origen_id, tipo)` Índices adicionales: `genero_origen_origen`, `genero_origen_principal_unico`. Disparadores: - `genero_sin_ciclos_trg`: `CREATE TRIGGER genero_sin_ciclos_trg BEFORE INSERT OR UPDATE ON genero_origen FOR EACH ROW EXECUTE FUNCTION genero_sin_ciclos()` ### `genero_pais` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------- | --------------- | :--: | ------------------ | | `genero_id` | `text` | no | — | | `pais_codigo` | `character(2)` | no | — | | `relacion` | `text` | no | `'adoptado'::text` | | `nota` | `text` | sí | — | Claves y restricciones: - **FK genero_pais_genero_id_genero_id_fk:** `FOREIGN KEY (genero_id) REFERENCES genero(id) ON DELETE CASCADE` - **FK genero_pais_pais_codigo_pais_codigo_fk:** `FOREIGN KEY (pais_codigo) REFERENCES pais(codigo)` - **PK genero_pais_genero_id_pais_codigo_pk:** `PRIMARY KEY (genero_id, pais_codigo)` ### `genero_persona` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `genero_id` | `text` | no | — | | `persona_id` | `text` | no | — | | `papel` | `text` | no | — | | `nota` | `text` | sí | — | | `orden` | `integer` | no | `0` | Claves y restricciones: - **FK genero_persona_genero_id_genero_id_fk:** `FOREIGN KEY (genero_id) REFERENCES genero(id) ON DELETE CASCADE` - **FK genero_persona_persona_id_persona_id_fk:** `FOREIGN KEY (persona_id) REFERENCES persona(id)` - **PK genero_persona_genero_id_persona_id_papel_pk:** `PRIMARY KEY (genero_id, persona_id, papel)` ### `genero_seccion` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ----------- | --------------- | :--: | ----------------- | | `id` | `text` | no | — | | `genero_id` | `text` | no | — | | `clave` | `text` | no | — | | `titulo` | `text` | no | — | | `cuerpo` | `text` | no | — | | `orden` | `integer` | no | `0` | Claves y restricciones: - **FK genero_seccion_genero_id_genero_id_fk:** `FOREIGN KEY (genero_id) REFERENCES genero(id) ON DELETE CASCADE` - **PK genero_seccion_pkey:** `PRIMARY KEY (id)` Índices adicionales: `genero_seccion_clave`. ### `genero_tema` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------- | --------------- | :--: | ----------------- | | `genero_id` | `text` | no | — | | `etiqueta_id` | `text` | no | — | | `orden` | `integer` | no | `0` | Claves y restricciones: - **FK genero_tema_etiqueta_id_etiqueta_id_fk:** `FOREIGN KEY (etiqueta_id) REFERENCES etiqueta(id) ON DELETE CASCADE` - **FK genero_tema_genero_id_genero_id_fk:** `FOREIGN KEY (genero_id) REFERENCES genero(id) ON DELETE CASCADE` - **PK genero_tema_genero_id_etiqueta_id_pk:** `PRIMARY KEY (genero_id, etiqueta_id)` ### `instrumento` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | --------------- | -------------------------- | :--: | ------------------ | | `id` | `text` | no | — | | `slug` | `text` | no | — | | `nombre` | `text` | no | — | | `familia` | `text` | no | — | | `subfamilia` | `text` | sí | — | | `pais_codigo` | `character(2)` | sí | — | | `descripcion` | `text` | sí | — | | `afinacion` | `text` | sí | — | | `imagen_id` | `text` | sí | — | | `muestra_id` | `text` | sí | — | | `estado` | `text` | no | `'borrador'::text` | | `origen` | `text` | no | `'manual'::text` | | `modelo_origen` | `text` | sí | — | | `importado_en` | `timestamp with time zone` | sí | — | | `revisado_por` | `text` | sí | — | | `revisado_en` | `timestamp with time zone` | sí | — | Claves y restricciones: - **FK instrumento_imagen_id_medio_id_fk:** `FOREIGN KEY (imagen_id) REFERENCES medio(id) ON DELETE SET NULL` - **FK instrumento_muestra_id_medio_id_fk:** `FOREIGN KEY (muestra_id) REFERENCES medio(id) ON DELETE SET NULL` - **FK instrumento_pais_codigo_pais_codigo_fk:** `FOREIGN KEY (pais_codigo) REFERENCES pais(codigo)` - **PK instrumento_pkey:** `PRIMARY KEY (id)` - **UNIQUE instrumento_slug_unique:** `UNIQUE (slug)` Índices adicionales: `instrumento_familia`. ### `metro` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `id` | `text` | no | — | | `codigo` | `text` | no | — | | `nombre` | `text` | no | — | | `silabas` | `integer` | sí | — | | `termino_id` | `text` | sí | — | | `nota` | `text` | sí | — | Claves y restricciones: - **FK metro_termino_fk:** `FOREIGN KEY (termino_id) REFERENCES termino(id) ON DELETE SET NULL` - **PK metro_pkey:** `PRIMARY KEY (id)` - **UNIQUE metro_codigo_unique:** `UNIQUE (codigo)` ### `modelo_ia` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | --------------- | --------------- | :--: | ----------------- | | `id` | `text` | no | — | | `codigo` | `text` | no | — | | `familia` | `text` | no | — | | `version` | `text` | no | — | | `nombre` | `text` | no | — | | `vigente` | `boolean` | no | `true` | | `nota_sintaxis` | `text` | sí | — | | `esquema` | `jsonb` | sí | — | Claves y restricciones: - **PK modelo_ia_pkey:** `PRIMARY KEY (id)` - **UNIQUE modelo_ia_codigo_unique:** `UNIQUE (codigo)` Índices adicionales: `modelo_ia_familia`. ### `obra_referencia` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | --------------- | -------------------------- | :--: | ------------------ | | `id` | `text` | no | — | | `slug` | `text` | no | — | | `titulo` | `text` | no | — | | `ano` | `integer` | sí | — | | `genero_id` | `text` | sí | — | | `nota` | `text` | sí | — | | `cita` | `text` | sí | — | | `estado` | `text` | no | `'borrador'::text` | | `origen` | `text` | no | `'manual'::text` | | `modelo_origen` | `text` | sí | — | | `importado_en` | `timestamp with time zone` | sí | — | | `revisado_por` | `text` | sí | — | | `revisado_en` | `timestamp with time zone` | sí | — | Claves y restricciones: - **FK obra_referencia_genero_id_genero_id_fk:** `FOREIGN KEY (genero_id) REFERENCES genero(id) ON DELETE SET NULL` - **PK obra_referencia_pkey:** `PRIMARY KEY (id)` - **UNIQUE obra_referencia_slug_unique:** `UNIQUE (slug)` Índices adicionales: `obra_referencia_genero`. ### `obra_referencia_enlace` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `obra_id` | `text` | no | — | | `plataforma` | `text` | no | — | | `url` | `text` | no | — | Claves y restricciones: - **FK obra_referencia_enlace_obra_id_obra_referencia_id_fk:** `FOREIGN KEY (obra_id) REFERENCES obra_referencia(id) ON DELETE CASCADE` - **PK obra_referencia_enlace_obra_id_plataforma_pk:** `PRIMARY KEY (obra_id, plataforma)` ### `obra_referencia_persona` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `obra_id` | `text` | no | — | | `persona_id` | `text` | no | — | | `papel` | `text` | no | — | | `orden` | `integer` | no | `0` | Claves y restricciones: - **FK obra_referencia_persona_obra_id_obra_referencia_id_fk:** `FOREIGN KEY (obra_id) REFERENCES obra_referencia(id) ON DELETE CASCADE` - **FK obra_referencia_persona_persona_id_persona_id_fk:** `FOREIGN KEY (persona_id) REFERENCES persona(id)` - **PK obra_referencia_persona_obra_id_persona_id_papel_pk:** `PRIMARY KEY (obra_id, persona_id, papel)` ### `prompt` Prompt probado para un modelo de IA, género, ánimo y, opcionalmente, una canción resultante. | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ---------------- | -------------------------- | :--: | ------------------ | | `id` | `text` | no | — | | `modelo_ia_id` | `text` | no | — | | `genero_id` | `text` | sí | — | | `animo_id` | `text` | sí | — | | `idioma` | `text` | no | `'en'::text` | | `contenido` | `jsonb` | no | — | | `notas` | `text` | sí | — | | `valoracion` | `integer` | sí | — | | `ejemplo_id` | `text` | sí | — | | `preferido` | `boolean` | no | `false` | | `visibilidad` | `text` | no | `'privado'::text` | | `estado` | `text` | no | `'borrador'::text` | | `creado_en` | `timestamp with time zone` | no | `now()` | | `actualizado_en` | `timestamp with time zone` | sí | — | | `cancion_id` | `text` | sí | — | Claves y restricciones: - **FK prompt_animo_id_animo_id_fk:** `FOREIGN KEY (animo_id) REFERENCES animo(id)` - **FK prompt_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE CASCADE` - **FK prompt_ejemplo_id_medio_id_fk:** `FOREIGN KEY (ejemplo_id) REFERENCES medio(id) ON DELETE SET NULL` - **FK prompt_genero_id_genero_id_fk:** `FOREIGN KEY (genero_id) REFERENCES genero(id) ON DELETE CASCADE` - **FK prompt_modelo_ia_id_modelo_ia_id_fk:** `FOREIGN KEY (modelo_ia_id) REFERENCES modelo_ia(id)` - **PK prompt_pkey:** `PRIMARY KEY (id)` Índices adicionales: `prompt_animo`, `prompt_cancion`, `prompt_genero`, `prompt_modelo`. ### `termino` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | --------- | --------------- | :--: | ----------------- | | `id` | `text` | no | — | | `slug` | `text` | no | — | | `termino` | `text` | no | — | Claves y restricciones: - **PK termino_pkey:** `PRIMARY KEY (id)` - **UNIQUE termino_slug_unique:** `UNIQUE (slug)` - **UNIQUE termino_termino_unique:** `UNIQUE (termino)` ### `termino_variante` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ------------ | --------------- | :--: | ----------------- | | `termino_id` | `text` | no | — | | `forma` | `text` | no | — | Claves y restricciones: - **FK termino_variante_termino_id_termino_id_fk:** `FOREIGN KEY (termino_id) REFERENCES termino(id) ON DELETE CASCADE` - **PK termino_variante_termino_id_forma_pk:** `PRIMARY KEY (termino_id, forma)` ### `verso` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | --------------- | -------------------------- | :--: | ------------------ | | `id` | `text` | no | — | | `genero_id` | `text` | no | — | | `titulo` | `text` | no | — | | `ilustra` | `text` | no | — | | `fuente` | `text` | no | `'ejemplo'::text` | | `analisis` | `text` | sí | — | | `medida` | `text` | sí | — | | `rima` | `text` | sí | — | | `obra_id` | `text` | sí | — | | `cancion_id` | `text` | sí | — | | `orden` | `integer` | no | `0` | | `estado` | `text` | no | `'borrador'::text` | | `origen` | `text` | no | `'manual'::text` | | `modelo_origen` | `text` | sí | — | | `importado_en` | `timestamp with time zone` | sí | — | | `revisado_por` | `text` | sí | — | | `revisado_en` | `timestamp with time zone` | sí | — | Claves y restricciones: - **CHECK verso_cita_acredita:** `CHECK (fuente <> 'cita'::text OR obra_id IS NOT NULL)` - **CHECK verso_propio_apunta:** `CHECK (fuente <> 'propio'::text OR cancion_id IS NOT NULL)` - **FK verso_cancion_id_cancion_id_fk:** `FOREIGN KEY (cancion_id) REFERENCES cancion(id) ON DELETE SET NULL` - **FK verso_genero_id_genero_id_fk:** `FOREIGN KEY (genero_id) REFERENCES genero(id) ON DELETE CASCADE` - **FK verso_obra_id_obra_referencia_id_fk:** `FOREIGN KEY (obra_id) REFERENCES obra_referencia(id) ON DELETE SET NULL` - **PK verso_pkey:** `PRIMARY KEY (id)` Índices adicionales: `verso_genero`. ### `verso_linea` | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ---------- | --------------- | :--: | ----------------- | | `verso_id` | `text` | no | — | | `numero` | `integer` | no | — | | `texto` | `text` | no | — | | `silabas` | `text[]` | sí | — | | `tonicas` | `integer[]` | sí | — | | `medida` | `integer` | sí | — | | `rima` | `text` | sí | — | Claves y restricciones: - **CHECK verso_linea_medida_creible:** `CHECK (medida IS NULL OR medida >= 1 AND medida <= 20)` - **FK verso_linea_verso_id_verso_id_fk:** `FOREIGN KEY (verso_id) REFERENCES verso(id) ON DELETE CASCADE` - **PK verso_linea_verso_id_numero_pk:** `PRIMARY KEY (verso_id, numero)` ## Jukebox ### `jukebox` Programación independiente y versionada de una cuenta. | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ---------------- | -------------------------- | :--: | ----------------- | | `id` | `text` | no | — | | `usuario_id` | `text` | no | — | | `numero` | `integer` | no | — | | `zona_horaria` | `text` | no | — | | `franjas` | `jsonb` | no | — | | `revision` | `integer` | no | `1` | | `actualizada_en` | `timestamp with time zone` | no | `now()` | | `nombre` | `text` | no | `''::text` | | `descripcion` | `text` | no | `''::text` | Claves y restricciones: - **CHECK jukebox_franjas_array:** `CHECK (jsonb_typeof(franjas) = 'array'::text)` - **CHECK jukebox_revision_positiva:** `CHECK (revision > 0)` - **FK jukebox_usuario_id_fkey:** `FOREIGN KEY (usuario_id) REFERENCES usuario(id) ON DELETE CASCADE` - **PK jukebox_pkey:** `PRIMARY KEY (id)` Índices adicionales: `jukebox_usuario_numero`. ### `programacion_jukebox` Programación histórica por cuenta, conservada por compatibilidad con el modelo anterior de un único Jukebox. | Columna | Tipo PostgreSQL | Nulo | Valor por defecto | | ---------------- | -------------------------- | :--: | ----------------------- | | `usuario_id` | `text` | no | — | | `zona_horaria` | `text` | no | `'Europe/Madrid'::text` | | `franjas` | `jsonb` | no | `'[]'::jsonb` | | `revision` | `integer` | no | `1` | | `actualizada_en` | `timestamp with time zone` | no | `CURRENT_TIMESTAMP` | Claves y restricciones: - **CHECK programacion_jukebox_franjas_array:** `CHECK (jsonb_typeof(franjas) = 'array'::text)` - **CHECK programacion_jukebox_revision_positiva:** `CHECK (revision > 0)` - **FK programacion_jukebox_usuario_id_fkey:** `FOREIGN KEY (usuario_id) REFERENCES usuario(id) ON DELETE CASCADE` - **PK programacion_jukebox_pkey:** `PRIMARY KEY (usuario_id)` ## Vistas ### `cancion_principal` | Columna | Tipo PostgreSQL | Nulo | | --------------------- | -------------------------- | :--: | | `id` | `text` | sí | | `slug` | `text` | sí | | `titulo` | `text` | sí | | `version_de_id` | `text` | sí | | `version_nombre` | `text` | sí | | `version_orden` | `integer` | sí | | `estilo_id` | `text` | sí | | `album_id` | `text` | sí | | `numero` | `integer` | sí | | `duracion_s` | `integer` | sí | | `ano` | `integer` | sí | | `letra` | `text` | sí | | `idioma` | `text` | sí | | `inspiracion` | `text` | sí | | `tiene_letra` | `boolean` | sí | | `destacada` | `boolean` | sí | | `portada_id` | `text` | sí | | `ala_venta` | `boolean` | sí | | `estado` | `text` | sí | | `creado_en` | `timestamp with time zone` | sí | | `actualizado_en` | `timestamp with time zone` | sí | | `es_version` | `boolean` | sí | | `padre_no_es_version` | `boolean` | sí | | `version_nota` | `text` | sí | | `codigo` | `text` | sí | | `version_codigo` | `text` | sí | | `formacion_vocal` | `formacion_vocal` | sí | | `acceso` | `acceso_cancion` | sí | Definición: ```sql SELECT id, slug, titulo, version_de_id, version_nombre, version_orden, estilo_id, album_id, numero, duracion_s, ano, letra, idioma, inspiracion, tiene_letra, destacada, portada_id, ala_venta, estado, creado_en, actualizado_en, es_version, padre_no_es_version, version_nota, codigo, version_codigo, formacion_vocal, acceso FROM cancion WHERE version_de_id IS NULL; ``` ## Enumeraciones PostgreSQL - `acceso_cancion`: `ninguna`, `publico`, `exclusiva`, `privada`. - `formacion_vocal`: `hombre`, `mujer`, `dueto-femenino`, `dueto-masculino`, `dueto-mixto`, `multiple-mixto`, `multiple-femenino`, `multiple-masculino`. Los demás vocabularios cerrados se almacenan como `text` y aparecen en las restricciones `CHECK` de cada tabla. ## Funciones de base de datos | Función | Argumentos | Resultado | Lenguaje | | --------------------------------- | -------------------- | --------- | --------- | | `autoria_suma_cien` | — | `trigger` | `plpgsql` | | `comprobar_grabacion_publicable` | `identificador text` | `void` | `plpgsql` | | `genero_sin_ciclos` | — | `trigger` | `plpgsql` | | `guardar_invariantes_publicacion` | — | `trigger` | `plpgsql` | Las funciones asociadas a disparadores se localizan además bajo la tabla afectada, en el apartado “Disparadores”. ## Secuencias | Secuencia | Tipo | Inicio | Incremento | Ciclo | | ---------------------------- | -------- | -----: | ---------: | :---: | | `artista_codigo_seq` | `bigint` | 1 | 1 | no | | `artista_virtual_codigo_seq` | `bigint` | 1 | 1 | no | | `cancion_codigo_seq` | `bigint` | 1 | 1 | no | ## Mantenimiento Este documento se regenera desde la base aplicada: ```powershell node --env-file=.env scripts/db/documentar-modelo.mjs ``` Después de una migración, regénéralo y revisa también los contratos JSON si cambió su estructura TypeScript.