Restricción: una fila booleana es verdadera, todas las demás filas son falsas


13

Tengo una columna: standard BOOLEAN NOT NULL

Me gustaría imponer una fila Verdadero y todas las demás Falso. No hay FK ni nada más dependiendo de esta restricción. Sé que puedo lograrlo con plpgsql, pero esto parece un mazo. Preferiría algo como a CHECKo UNIQUErestricción. Cuanto más simple, mejor.

Una fila debe ser verdadera, no todas pueden ser falsas (por lo que la primera fila insertada debería ser verdadera).

La fila deberá actualizarse, lo que significa que tengo que esperar para verificar las restricciones hasta que se realicen las actualizaciones, ya que todas las filas se pueden configurar como False primero y una fila True después.

Hay un FK entre products.tax_rate_idy tax_rate.id, pero no tiene nada que ver con la tasa impositiva predeterminada o estándar, que puede seleccionar el usuario para facilitar la creación de nuevos productos.

PostgreSQL 9.5 si es importante.

Antecedentes

La tabla es la tasa de impuestos. Una de las tasas impositivas es la predeterminada ( standardya que la predeterminada es un comando de Postgres). Cuando se agrega un nuevo producto, la tasa impositiva estándar se aplica al producto. Si no hay standard, la base de datos debe hacer una conjetura o todo tipo de comprobaciones innecesarias. La solución simple, pensé, era asegurarme de que haya una standard.

Por "predeterminado" arriba, me refiero a la capa de presentación (IU). Hay una opción de usuario para cambiar la tasa impositiva predeterminada. Necesito agregar controles adicionales para asegurar que la GUI / usuario no intente establecer tax_rate_id en NULL, o simplemente establecer una tasa impositiva predeterminada.


Entonces, ¿tienes tu respuesta?
Erwin Brandstetter

Sí, tengo mi respuesta, muchas gracias por su aporte, @ErwinBrandstetter. Me estoy inclinando hacia un gatillo por ahora. Este es un proyecto de código abierto en mi propio tiempo. Cuando realmente lo implemente, marcaré la respuesta aceptada que uso.
theGtknerd

Respuestas:


15

Variante 1

Como todo lo que necesita es una sola columna standard = true, establezca el estándar en NULL en todas las demás filas. Entonces funciona una UNIQUErestricción simple , ya que los valores NULL no la violan:

CREATE TABLE taxrate (
   taxrate int PRIMARY KEY
 , standard bool DEFAULT true
 , CONSTRAINT standard_true_or_null CHECK (standard) -- yes, that's the whole constraint
 , CONSTRAINT standard_only_1_true UNIQUE (standard)
);

DEFAULTes un recordatorio opcional de que la primera fila ingresada debe convertirse en la predeterminada. No está haciendo cumplir nada. Si bien no puede establecer más de una fila en standard = true, aún puede establecer todas las filas como NULL. No hay una forma limpia de evitar esto con solo restricciones en una sola tabla. CHECKLas restricciones no consideran otras filas (sin trucos sucios).

Relacionado:

Actualizar:

BEGIN;
UPDATE taxrate SET standard = NULL WHERE standard;
UPDATE taxrate SET standard = TRUE WHERE taxrate = 2;
COMMIT;

Para permitir un comando como (donde la restricción solo se cumple al final de la declaración):

WITH kingdead AS (
   UPDATE taxrate
   SET standard = NULL
   WHERE standard
   )
UPDATE taxrate
SET standard = TRUE
WHERE taxrate = 1;

.. la UNIQUErestricción tendría que ser DEFERRABLE. Ver:

dbfiddle aquí

Variante 2

Tenga una segunda tabla con una sola fila como:

Crea esto como superusuario:

CREATE TABLE taxrate (
   taxrate int PRIMARY KEY
);

CREATE TABLE taxrate_standard (
   taxrate int PRIMARY KEY REFERENCES taxrate
);

CREATE UNIQUE INDEX taxrate_standard_singleton ON taxrate_standard ((true));  -- singleton

REVOKE DELETE ON TABLE taxrate_standard FROM public;  -- can't delete

INSERT INTO taxrate (taxrate) VALUES (42);
INSERT INTO taxrate_standard (taxrate) VALUES (42);

Ahora siempre hay una única fila que apunta al estándar (en este caso simple que también representa la tasa estándar directamente). Solo un superusuario podría romperlo. También puede rechazar eso con un disparador BEFORE DELETE.

dbfiddle aquí

Relacionado:

Puede agregar a VIEWpara ver lo mismo que en la variante 1 :

CREATE VIEW taxrate_combined AS
SELECT t.*, (ts.taxrate = t.taxrate) AS standard
FROM   taxrate t
LEFT   JOIN taxrate_standard ts USING (taxrate);

En consultas donde lo único que desea es la tarifa estándar, use (solo) taxrate_standard.taxratedirectamente.


Luego agregaste:

Hay un FK entre products.tax_rate_idytax_rate.id

Una aplicación del pobre hombre de la variante 2 sería simplemente añadir una fila a products(o cualquier tabla similar) señalando el tipo general de gravamen; un producto ficticio que podría llamar "tasa impositiva estándar", si su configuración lo permite.

Las restricciones de FK hacen cumplir la integridad referencial. Para completarlo, aplique tax_rate_id IS NOT NULLla fila (si ese no es el caso de la columna en general). Y no permitir su eliminación. Ambos podrían ser puestos en disparadores. No hay mesa extra, pero menos elegante y no tan confiable.


2
Altamente recomendar el enfoque de dos mesas. También sugeriría agregar una consulta de ejemplo a esa variación para que el OP pueda ver cómo CROSS JOINcomparar el estándar, LEFT JOINel específico y luego COALESCEentre los dos.
jpmc26

2
+1, tuve la misma idea sobre la tabla extra pero no tuve tiempo para escribir una respuesta correctamente. Acerca de la primera tabla y CONSTRAINT standard_only_1_true UNIQUE (standard): Supongo que la tabla no será grande, por lo que no importa mucho, pero dado que la restricción definirá un índice en toda la tabla, ¿no sería un índice único parcial con WHERE (standard)menos espacio?
ypercubeᵀᴹ

@ ypercubeᵀᴹ: Sí, el índice en toda la tabla es más grande, eso es un inconveniente para esta variante. Pero como dijiste: obviamente es una mesa pequeña, por lo que apenas importa. Estaba apuntando a la solución estándar más simple con solo restricciones. Prueba de concepto. Personalmente, estoy con jpmc26 y estoy fuertemente a favor de la variante 2.
Erwin Brandstetter

9

Puede usar un índice filtrado

create table test
(
    id int primary key,
    foo bool
);
CREATE UNIQUE INDEX only_one_row_with_column_true_uix 
    ON test (foo) WHERE (foo);  --> where foo is true
insert into test values (1, false);
insert into test values (2, true);
insert into test values (3, false);
insert into test values (4, false);
insert into test values (5, true);
ERROR: el valor de clave duplicada viola la restricción única "only_one_row_with_column_true_uix"
DETALLE: La clave (foo) = (t) ya existe.

dbfiddle aquí


Pero como dijiste, la primera fila debe ser verdadera, entonces podrías usar una restricción CHECK, pero incluso usando una función puedes eliminar la primera fila más tarde.

create function check_one_true(new_foo bool)
returns int as
$$
begin
    return 
    (
        select count(*) + (case new_foo when true then 1 else 0 end)
        from test 
        where foo = true
    );
end
$$
language plpgsql stable;
alter table test 
    add constraint ck_one_true check(check_one_true(foo) = 1); 
insert into test values (1, true);
insert into test values (2, false);
insert into test values (3, false);
insert into test values (4, false);
insert into test values (5, true);
ERROR: la nueva fila para la relación "prueba" viola la restricción de verificación "ck_one_true"
DETALLE: La fila que falla contiene (5, t).

select * from test;
id | foo
-: | : -
 1 | t  
 2 | F  
 3 | F  
 4 | F  
delete from test where id = 1;

dbfiddle aquí


Puede resolverlo agregando un disparador ANTES DE BORRAR para asegurarse de que la primera fila (foo es verdadera) nunca se elimine.

create function dont_delete_foo_true()
returns trigger as
$x$
begin
    if old.foo then
        raise exception 'Can''t delete row where foo is true.';
    end if;
    return old;
end;
$x$ language plpgsql;
create trigger trg_test_delete
before delete on test
for each row 
execute procedure dont_delete_foo_true();
delete from test where id = 1;

ERROR: no se puede eliminar la fila donde foo es cierto.

dbfiddle aquí

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.