Decisiones transversales¶
D-01¶
Llave primaria
uuidconDEFAULT 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
timestamptzalmacenado 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_reservadacalificacion_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 |