Saltar a contenido

Decisiones transversales

D-01

Llave primaria uuid con DEFAULT uuidv7()

Ordenable en el tiempo, con localidad de índice equivalente a un serial; no enumerable desde fuera; generable en el cliente móvil sin coordinación. Nativo en PostgreSQL 18, sin extensiones.

D-02

Todo instante en timestamptz almacenado en UTC

La fecha de negocio se materializa aparte como fecha_operativa date, resuelta en America/Managua.

Evita que "el día" dependa de la zona del cliente y permite indexar los tableros diarios sin funciones sobre la columna, tal como exige el reloj autoritativo del servidor. Nicaragua no aplica horario de verano, lo que hace la conversión estable; expandir a otro país obligaría a revisarla.

D-03

Dinero en numeric(10,2)

Se usa NIO como moneda implícita con un CHECK de no negatividad.

RF-C-30 exige hasta dos decimales y RF-A-51 agrega miles de montos. La coma flotante produce totales que no cuadran contra la suma manual del comercio.

D-04

Geografía con PostGIS

Toda ubicación se define con geography(Point,4326) y un índice GiST.

RF-A-31 exige polígonos de cobertura y RF-U-19 filtros por radio real de 1, 3, 5 y 10 km. earthdistance resuelve radios pero no polígonos.

D-05

Texto y colecciones nunca son nulables

El valor de ausencia es '', [] o {}.

Dos valores vacíos obligan a escribir col IS NOT NULL AND col <> '' en cada predicado, y basta olvidar una mitad para que la consulta mienta. Además el nulo se propaga por concatenación y comparación, de modo que una sola columna nulable envenena toda expresión derivada que la use. Los índices y restricciones parciales sobre texto usan length(col) > 0, que es exacto. El nulo se reserva para los tipos donde no existe centinela honesto: instantes, fechas, montos, cantidades y geografía.

D-06

Nunca se declara una llave primaria natural

Una llave primaria natural compuesta se propaga: toda tabla que la referencie repite sus columnas, y cada nivel acumula las del anterior. Con una llave sustituta la referencia es siempre una columna, y la fila conserva un identificador estable que sirve de ruta pública aunque sus atributos naturales cambien. Las relaciones 1:1 se expresan con un índice único sobre la llave foránea, que da exactamente la misma garantía.

D-07

Inmutabilidad forzada con triggers BEFORE

Lanzan excepción en el motor de base de datos, no validando reglas en la capa de aplicación.

RF-C-41 y RF-A-59 establecen que ni el superadministrador puede borrar. Una regla que vive en un servicio se evade desde psql; una que vive en el trigger, no.

D-08

Predicados de índice parcial

Siempre se derivan de marcas temporales de la propia fila, nunca del estado.

El predicado de un índice solo admite expresiones inmutables sobre columnas de la misma fila: no puede consultar estado_<entidad> ni llamar a now(). Cada entidad con ciclo de vida lleva ya las marcas que registran sus transiciones por obligación de auditoría, así que el subconjunto vivo se identifica con IS NULL sobre la marca de cierre sin agregar ninguna columna.

Es lo que sostiene el límite de reservas activas bajo concurrencia: un índice único parcial sobre WHERE finalizada_at IS NULL lo garantiza en el motor, mientras que una comprobación previa dejaría pasar dos peticiones simultáneas. La correspondencia entre cada estado y sus marcas la impone un trigger por tabla.

D-09

Máquinas de estado

Se definen tablas de transiciones legales para reserva, publicacion y comercio que se imponen con un trigger.

Son las tres máquinas con ramas, actores distintos y consecuencias irreversibles. Una transición ilegal de reserva produce entregas duplicadas o stock fantasma. El resto de los dominios tiene ciclos lineales sin ramificación, donde una tabla de transiciones sería mecanismo sin regla que imponer.

D-10

Coherencia de columnas denormalizadas

Se imponen copiando el valor una sola vez y protegiendo la columna después con un trigger de solo lectura, no con una llave foránea compuesta.

Una llave foránea compuesta obliga al padre a cargar un índice único redundante sobre columnas que ya dependen funcionalmente de su llave primaria. Como las columnas denormalizadas del modelo (reserva.comercio_id, publicacion.precio, valoracion.comercio_id) se escriben una única vez al crear la fila y después son inmutables, el trigger de solo lectura da la misma garantía sin el índice extra: la columna no puede divergir porque no puede escribirse desde fuera.

D-11

Una sola tabla de cuentas

Usada por consumidores, personal de comercio y administradores, con perfiles y membresías separadas.

RF-U-04 y RF-C-05 exigen unicidad global de correo y teléfono; con tablas separadas esa unicidad cruzada no es garantizable en la base. Además, una misma persona puede ser consumidora y operadora de un comercio. Las políticas distintas de contraseña y sesión (RF-U-07 frente a RF-C-11 y RF-A-02) se derivan del ámbito de la sesión, no de la tabla que guarda la cuenta.

D-12

Estados del ciclo de vida como tablas de catálogos

Agregar un estado o renombrar una etiqueta visible es un INSERT o un UPDATE, no una migración con DROP CONSTRAINT y ADD CONSTRAINT. Solo se tiene que mencionar codigo, que es inmutable; todo lo demás (etiqueta, descripción, atributos semánticos) es configuración. El patrón completo y el catálogo de los catorce dominios están en Estados.

D-13

Catálogos administrables como tablas

Siempre validadas con activo, nunca como enumeraciones del código.

Se referencian con una llave foránea ON DELETE RESTRICT.

RF-A-45 y RF-A-46 exigen alta, edición y desactivación sin despliegue, y prohíben borrar valores en uso histórico.

D-14

Clasificaciones de enumeraciones

Las enumeraciones cerradas se clasifican en tres clases: estado, discriminador estructural y catálogo administrable. Cada una recibe un mecanismo distinto.

Convertir en tabla un discriminador como zona_cobertura.tipo prometería una extensibilidad inexistente: agregar un valor exige columnas nuevas que solo aplican a ese valor, así que la tabla solo escondería que el modelo no es extensible en ese eje. La taxonomía y la prueba para aplicarla están en Estados.

D-15

Snapshot obligatorio de todo dato volátil

Ocurre dentro del registro que lo consume: la reserva congela precio, ventana, nombre de oferta y monto; la publicación congela sus cinco instantes ya resueltos y los datos de la plantilla.

RF-U-40 exige un comprobante consultable indefinidamente y RF-C-30 permite cambiar precios. Sin snapshot, un cambio de precio reescribe la historia contable.

D-16

Parámetros del sistema en tabla, con historial automático

Cada cambio queda rastreado por la tabla de eventos gemela de parametro_sistema, sin un modelo de historial aparte que mantener.

Los registros que dependen de un parámetro en el momento de crearse lo congelan en sus propias columnas: si mañana la tolerancia de validación pasa de 10 a 20 minutos, las validaciones de ayer deben seguir explicándose con 10, aunque el parámetro sea editable en caliente.

D-17

Contadores denormalizados

Ejemplos:

  • cantidad_reservada
  • calificacion_promedio

Se mantienen actualizados con triggers, con CHECK como red de seguridad en la base de datos.

RF-U-23 y RF-C-36 exigen lecturas de stock cada 30 segundos desde muchos clientes. El CHECK convierte una condición de carrera en un error de transacción en lugar de una sobreventa, incluso si algún camino llega a escribir el contador por fuera del trigger.

D-18

Nombres de dominio en español

El lenguaje ubicuo del negocio es español y los requerimientos están en español. Traducir "ventana de recogida" a pickup_window introduce un salto mental permanente en cada revisión.

D-19

Cada tabla declara en su ficha un régimen de auditoría

  • append-only
  • Mutable protegida
  • Mutable rastreada

Sin la declaración explícita, la elección de proteger o no una tabla se toma por omisión y nadie la revisa. Los tres regímenes y su implementación están en Convenciones.

D-20

Los alérgenos no son un dato del sistema

No hay catálogo, ni asociación por comercio, ni asociación por oferta, ni validación.

La declaración de alérgenos es responsabilidad del comercio y se expresa en el texto libre de su publicación. Modelarlo como catálogo cerrado crearía una obligación implícita; el sistema parecería garantizar que la lista es completa y correcta sobre un dato que un operador introduce y que la plataforma no puede verificar. La consecuencia es que RF-U-24 se cubre a través de la descripción de la oferta y que queda una exposición legal abierta.


Extensiones requeridas

uuidv7() es nativo de PostgreSQL 18 y no requiere extensión; ver funciones UUID.

unaccent no es inmutable y por lo tanto no puede aparecer en la expresión de un índice. El modelo la envuelve en immutable_unaccent, una función IMMUTABLE que fija el diccionario, y todos los índices GIN de búsqueda por texto se declaran sobre upper(immutable_unaccent(columna)).

Extensión Uso
btree_gist EXCLUDE con igualdad y solapamiento
pg_trgm Búsqueda por similitud en nombres
postgis geography, ST_DWithin, polígonos
unaccent Búsqueda insensible a tildes