Hi all
I am afraid, I have not understood the rule system yet.
I have got 4 tables and a view over all of them.
ladevorgaenge kanton tarifgruppe
0..1 \ | /
\ | /
\ | /
| | |
/|\ /|\ /|\
tarif_progressiv
An DML onto the view tarif_progressiv_denorm is not possible because of
the joins. So I intended to create rules to handle that. However, the do
not behave as I expect.
INSERT INTO
"budget"."tarif_progressiv_denorm"
(
"kantonscode",
"kantonsname",
"tarifgruppencode",
"kinder_anzahl",
"kirchensteuer_flag",
"einkommen_steuerbares_range_low_boundary",
"einkommen_steuerbares_range_high_boundary",
"tarifschritt",
"mindeststeuer",
"steuersatz",
"ladevorgaenge⠒id"
)
VALUES
(
'AG',
null,
'M',
1,
false,
10,
30,
10,
10,
20,
'0562b97a-87af-4071-b56d-f25b4e9bca0f'
);
a) Unexpected not-null constraint violationfor kanton⠒id
update tarif_progressiv_denorm
set kantonscode = 'BE'
where kantonscode = 'AG';
update tarif_progressiv_denorm
set kantonsname = 'Bern'
where kantonsname = 'Aargau';
update tarif_progressiv_denorm
set kantonsname = 'Zürich'
where kantonscode = 'AG';
b) I thought the following would throw a not-null constraint
violationbecause kanton⠒id must not be null.
update tarif_progressiv_denorm
set kantonscode = null
where kantonscode = 'AG';
c) I noticed that, even though the rules define logic for the other
attributes, those do not get changed if not present in an update. While
this is actually good, but surprises me nonetheless.
Did I miss some reading in the doc? Must I use triggers instead?
Btw, I am using DbVis against PostgreSQL 16 for the queries.
Kind regards
Thiemo
drop table if exists BUDGET.TARIF_PROGRESSIV cascade;
create table
BUDGET.TARIF_PROGRESSIV (ID uuid
default gen_random_uuid()
constraint PK⠒TG⠒ID
primary key,
KANTON⠒ID uuid
not null
constraint FK⠒TP⠒K⠒ID
references BUDGET_MASTER.KANTON (ID),
TARIFGRUPPE⠒ID uuid
not null
constraint FK⠒TP⠒TG⠒ID
references BUDGET_MASTER.TARIFGRUPPE (ID),
KINDER_ANZAHL smallint
not null,
KIRCHENSTEUER_FLAG boolean
not null,
EINKOMMEN_STEUERBARES_RANGE numrange
not null,
TARIFSCHRITT numeric(1000, 2)
not null,
MINDESTSTEUER numeric(1000, 2)
not null,
STEUERSATZ numeric(1000, 998)
not null,
LADEVORGAENGE⠒ID uuid
constraint FK⠒TP⠒LV⠒ID
references BUDGET_LOG.LADEVORGAENGE (ID),
constraint UK⠒TP⠒UQ unique (KANTON⠒ID,
TARIFGRUPPE⠒ID,
KINDER_ANZAHL,
KIRCHENSTEUER_FLAG,
EINKOMMEN_STEUERBARES_RANGE));
comment on column BUDGET.TARIF_PROGRESSIV.ID is
'Künstlicher Schlüssel';
comment on column BUDGET.TARIF_PROGRESSIV.KINDER_ANZAHL is
'';
comment on column BUDGET.TARIF_PROGRESSIV.KIRCHENSTEUER_FLAG is
'Muss Kirchensteuer eingerechnet werden';
comment on column BUDGET.TARIF_PROGRESSIV.EINKOMMEN_STEUERBARES_RANGE is
'';
comment on column BUDGET.TARIF_PROGRESSIV.TARIFSCHRITT is
'';
comment on column BUDGET.TARIF_PROGRESSIV.MINDESTSTEUER is
'';
comment on column BUDGET.TARIF_PROGRESSIV.STEUERSATZ is
'';
comment on table BUDGET.TARIF_PROGRESSIV is
'Tarife progressiver Quellensteuer
$Id$';
commit; -- In contrast to Oracle, ddls do not commit implicitly.
drop table if exists BUDGET_LOG.LADEVORGAENGE cascade;
create table if not exists
BUDGET_LOG.LADEVORGAENGE (ID uuid
default gen_random_uuid()
constraint PK⠒LV⠒ID primary key,
LADEVORGANG_PT timestamp
default current_timestamp
not null
constraint UK⠒LV⠒LADEVORGANG_PT unique);
comment on table BUDGET_LOG.LADEVORGAENGE is
'Logtabelle, welche die Zeitpunkte von Ladevorgängen enthält
$Id: ladevorgaenge.pg_sql 2639 2023-10-26 16:32:50Z thiemo $';
comment on column BUDGET_LOG.LADEVORGAENGE.ID is
'Primärschlüssel';
comment on column BUDGET_LOG.LADEVORGAENGE.LADEVORGANG_PT is
'Zeitpunkt, wann der Ladevorgang stattgefunden hat';
commit;
drop table if exists BUDGET_MASTER.TARIFGRUPPE cascade;
create table if not exists
BUDGET_MASTER.TARIFGRUPPE (ID uuid
default gen_random_uuid()
constraint PK⠒TG⠒ID
primary key,
CODE varchar(2)
not null
constraint UK⠒TG⠒CODE
unique,
BESCHREIBUNG text
not null
constraint UK⠒TG⠒BESCHREIBUNG
unique,
LADEVORGAENGE⠒ID uuid
constraint FK⠒TG⠒LV⠒ID
references BUDGET_LOG.LADEVORGAENGE (ID));
insert into BUDGET_MASTER.TARIFGRUPPE (CODE, BESCHREIBUNG)
values ('A', 'Tarif für alleinstehende Personen'),
('B', 'Tarif für verheiratete Alleinverdiener'),
('C', 'Tarif für verheiratete Doppelverdiener'),
('D', 'Tarif für Personen, denen Beiträge an die AHV
zurückerstattet werden'),
('E', 'Tarif für Einkünfte, die im vereinfachten
Abrechnungsverfahren besteuert werden'),
('F', 'Tarif für Grenzgänger aus Italien, deren Ehegatte ausserhalb
der Schweiz erwerbstätig ist'),
('G', 'Tarif für Ersatzeinkünfte, die nicht über die Arbeitgeber an
die quellensteuerpflichtigen Personen ausbezahlt werden'),
('H', 'Tarif für alleinstehende Personen, die mit Kindern oder
unterstützungsbedürftigen Personen im gleichen Haushalt zusammenleben und deren
Unterhalt zur Hauptsache bestreiten'),
('L', 'Tarif für Grenzgänger aus Deutschland, welche die
Voraussetzungen für den Tarifcode A erfüllen'),
('M', 'Tarif für Grenzgänger aus Deutschland, welche die
Voraussetzungen für den Tarifcode B erfüllen'),
('N', 'Tarif für Grenzgänger aus Deutschland, welche die
Voraussetzungen für den Tarifcode C erfüllen'),
('P', 'Tarif für Grenzgänger aus Deutschland, welche die
Voraussetzungen für den Tarifcode H erfüllen'),
('Q', 'Tarif für Grenzgänger aus Deutschland, welche die
Voraussetzungen für den Tarifcode G erfüllen'),
('R', 'Ausschließlich Tessin: Tarif Grenzgänger aus Italien, welche
die Voraussetzungen für den Tarifcode A erfüllen'),
('S', 'Ausschließlich Tessin: Tarif Grenzgänger aus Italien, welche
die Voraussetzungen für den Tarifcode B erfüllen'),
('T', 'Ausschließlich Tessin: Tarif Grenzgänger aus Italien, welche
die Voraussetzungen für den Tarifcode C erfüllen'),
('U', 'Ausschließlich Tessin: Tarif Grenzgänger aus Italien, welche
die Voraussetzungen für den Tarifcode H erfüllen'),
('HE', 'Tarif für Verwaltungsräte'),
('ME', 'Tarif für Mitarbeiterbeteiligungen'),
('NO', 'Im Falle einer Korrektur für Personen, die fälschlicherweise
an der Quelle besteuert wurden bzw. die fälschlicherweise nicht an der Quelle
besteuert wurden'),
('SF', 'Tarif für Grenzgänger aus Frankreich nach der
Sondervereinbarung der Kantone BE, BS, BL, JU, NE, SO, VD und VS');
-- CODE, BESCHREIBUNG
commit;
comment on column BUDGET_MASTER.TARIFGRUPPE.ID is
'Künstlicher Schlüssel';
comment on column BUDGET_MASTER.TARIFGRUPPE.CODE is
'Gruppe';
comment on column BUDGET_MASTER.TARIFGRUPPE.BESCHREIBUNG is
'in Deutsch';
comment on table BUDGET_MASTER.TARIFGRUPPE is
'Enumeration der Tarifgruppen
$Id$';
commit; -- In contrast to Oracle, ddls do not commit implicitly.
drop table if exists BUDGET_MASTER.KANTON cascade;
create table if not exists
BUDGET_MASTER.KANTON (ID uuid
default gen_random_uuid()
constraint PK⠒K⠒ID
primary key,
CODE varchar(2)
not null
constraint UK⠒K⠒CODE
unique,
NAME text
not null
constraint UK⠒K⠒NAME
unique,
LADEVORGAENGE⠒ID uuid
constraint FK⠒K⠒LV⠒ID
references BUDGET_LOG.LADEVORGAENGE (ID));
insert into BUDGET_MASTER.KANTON (CODE, NAME)
values ('AG', 'Aargau'),
('AI', 'Appenzell Innerrhoden'),
('AR', 'Appenzell Ausserrhoden'),
('BE', 'Bern'),
('BL', 'Basel-Landschaft'),
('BS', 'Basel-Stadt'),
('FR', 'Freiburg'),
('GE', 'Genf'),
('GL', 'Glarus'),
('GR', 'Graubünden'),
('JU', 'Jura'),
('LU', 'Luzern'),
('NE', 'Neuenburg'),
('NW', 'Nidwalden'),
('OW', 'Obwalden'),
('SG', 'St. Gallen'),
('SH', 'Schaffhausen'),
('SO', 'Solothurn'),
('SZ', 'Schwyz'),
('TG', 'Thurgau'),
('TI', 'Tessin'),
('UR', 'Uri'),
('VD', 'Waadt'),
('VS', 'Wallis'),
('ZG', 'Zug'),
('ZH', 'Zürich');
-- CODE, NAME
comment on column BUDGET_MASTER.KANTON.ID is
'Künstlicher Schlüssel';
comment on column BUDGET_MASTER.KANTON.CODE is
'Abkürzung des Kantonsnamen';
comment on column BUDGET_MASTER.KANTON.NAME is
'in Deutsch';
comment on table BUDGET_MASTER.KANTON is
'Enthält die Aufzöhlung aller Schweizer Kantone
$Id$';
commit; -- In contrast to Oracle, ddls do not commit implicitly.
drop view if exists BUDGET.TARIF_PROGRESSIV_DENORM;
create or replace view BUDGET.TARIF_PROGRESSIV_DENORM as
select K.CODE as KANTONSCODE,
K.NAME as KANTONSNAME,
TG.CODE as TARIFGRUPPENCODE,
TG.BESCHREIBUNG as TARIFGRUPPENBESCHREIBUNG,
TP.KINDER_ANZAHL,
TP.KIRCHENSTEUER_FLAG,
TP.EINKOMMEN_STEUERBARES_RANGE,
lower(TP.EINKOMMEN_STEUERBARES_RANGE) as
EINKOMMEN_STEUERBARES_RANGE_LOW_BOUNDARY,
upper(TP.EINKOMMEN_STEUERBARES_RANGE) as
EINKOMMEN_STEUERBARES_RANGE_HIGH_BOUNDARY,
TP.TARIFSCHRITT,
TP.MINDESTSTEUER,
TP.STEUERSATZ,
LV.LADEVORGANG_PT,
TP.LADEVORGAENGE⠒ID,
TP.ID
from BUDGET.TARIF_PROGRESSIV TP
inner join BUDGET_LOG.LADEVORGAENGE LV
on TP.LADEVORGAENGE⠒ID = LV.ID
inner join BUDGET_MASTER.KANTON K
on TP.KANTON⠒ID = K.ID
inner join BUDGET_MASTER.TARIFGRUPPE TG
on TP.TARIFGRUPPE⠒ID = TG.ID;
comment on column BUDGET.TARIF_PROGRESSIV_DENORM.KANTONSCODE is
'Abkürzung des Kantonsnamen';
comment on column BUDGET.TARIF_PROGRESSIV_DENORM.LADEVORGANG_PT is
'Zeitpunkt, wann der Ladevorgang war, der den Datensatz in die Tabelle
geschrieben hat. Dies ist unbesehen von Vorgängen, z. B. Konsolidierung, die
den Satz noch verändern.';
comment on view BUDGET.TARIF_PROGRESSIV_DENORM is
'Informationen progressiver Tarife angereichert mit Kantos-, Tarifgruppen-
und Ladeinformationen
$Id$';
-- create or replace function BUDGET.TARIF_PROGRESSIV_DENORM⠒INS_DEL() returns
trigger
-- as
-- $CODE$
-- BEGIN
-- raise warning 'Insert or deletes are not allowed on
TARIF_PROGRESSIV_DENORM. For inserts use the batch programme. Deletes should be
made as changes to the galus of WIEDERKEHREND.';
-- END;
-- $CODE$
-- language plpgsql;
-- create or replace trigger TARIF_PROGRESSIV_DENORM⠒INS_DEL
-- before insert or delete
-- on BUDGET.TARIF_PROGRESSIV_DENORM
-- for each statement
-- execute function BUDGET.TARIF_PROGRESSIV_DENORM⠒INS_DEL();
create or replace rule TARIF_PROGRESSIV_DENORM⠒INS as
on insert
to BUDGET.TARIF_PROGRESSIV_DENORM
do instead
insert into TARIF_PROGRESSIV (KANTON⠒ID,
TARIFGRUPPE⠒ID,
KINDER_ANZAHL,
KIRCHENSTEUER_FLAG,
EINKOMMEN_STEUERBARES_RANGE,
TARIFSCHRITT,
MINDESTSTEUER,
STEUERSATZ,
LADEVORGAENGE⠒ID)
values (/* KANTON⠒ID */ case
-- a disjoint
when new.KANTONSCODE is
not null and new.KANTONSNAME is null then (select ID
from BUDGET_MASTER.KANTON
where 1 = 1
and CODE = new.KANTONSCODE
and 1 = 1)
-- the other disjoint
when new.KANTONSCODE is
null and new.KANTONSNAME is not null then (select ID
from BUDGET_MASTER.KANTON
where 1 = 1
and NAME = new.KANTONSNAME
and 1 = 1)
-- check for conflicts
when new.KANTONSCODE is
not null and new.KANTONSNAME is not null then (select ID
from BUDGET_MASTER.KANTON
where 1 = 1
and CODE = new.KANTONSCODE
and NAME = new.KANTONSNAME
and 1 = 1)
-- fallback
else null
end,
/* TARIFGRUPPE⠒ID */ case
-- a disjoint
when new.TARIFGRUPPENCODE
is not null and new.TARIFGRUPPENBESCHREIBUNG is null then (select ID
from
BUDGET_MASTER.TARIFGRUPPE
where 1 = 1
and CODE =
new.TARIFGRUPPENCODE
and 1 = 1)
-- the other disjoint
when new.TARIFGRUPPENCODE
is null and new.TARIFGRUPPENBESCHREIBUNG is not null then (select ID
from
BUDGET_MASTER.TARIFGRUPPE
where 1 = 1
and BESCHREIBUNG
= new.TARIFGRUPPENBESCHREIBUNG
and 1 = 1)
-- check for conflicts
when new.TARIFGRUPPENCODE
is not null and new.TARIFGRUPPENBESCHREIBUNG is not null then (select ID
from
BUDGET_MASTER.TARIFGRUPPE
where 1 = 1
and CODE =
new.TARIFGRUPPENCODE
and
BESCHREIBUNG = new.TARIFGRUPPENBESCHREIBUNG
and 1 = 1)
-- fallback
else null
end,
/* KINDER_ANZAHL */ new.KINDER_ANZAHL,
/* KIRCHENSTEUER_FLAG */ new.KIRCHENSTEUER_FLAG,
/* EINKOMMEN_STEUERBARES_RANGE */ case
-- most frequent and
disjoint
when
new.EINKOMMEN_STEUERBARES_RANGE is null
and (
new.EINKOMMEN_STEUERBARES_RANGE_LOW_BOUNDARY is not null
or
new.EINKOMMEN_STEUERBARES_RANGE_HIGH_BOUNDARY is not null) then
numrange(new.EINKOMMEN_STEUERBARES_RANGE_LOW_BOUNDARY,
new.EINKOMMEN_STEUERBARES_RANGE_HIGH_BOUNDARY)
-- the other disjoint
when
new.EINKOMMEN_STEUERBARES_RANGE is not null
and
new.EINKOMMEN_STEUERBARES_RANGE_LOW_BOUNDARY is null
and
new.EINKOMMEN_STEUERBARES_RANGE_HIGH_BOUNDARY is null then
new.EINKOMMEN_STEUERBARES_RANGE
-- check for conflicts
when
new.EINKOMMEN_STEUERBARES_RANGE is not null
and
(new.EINKOMMEN_STEUERBARES_RANGE_LOW_BOUNDARY is not null or
new.EINKOMMEN_STEUERBARES_RANGE_HIGH_BOUNDARY is not null)
and
new.EINKOMMEN_STEUERBARES_RANGE =
numrange(new.EINKOMMEN_STEUERBARES_RANGE_LOW_BOUNDARY,
new.EINKOMMEN_STEUERBARES_RANGE_HIGH_BOUNDARY) then
new.EINKOMMEN_STEUERBARES_RANGE
-- fallback
else null
end,
/* TARIFSCHRITT */ new.TARIFSCHRITT,
/* MINDESTSTEUER */ new.MINDESTSTEUER,
/* STEUERSATZ */ new.STEUERSATZ,
/* LADEVORGAENGE⠒ID */ case
-- most frequent and
disjoint
when new.LADEVORGAENGE⠒ID
is not null
and new.LADEVORGANG_PT
is null then new.LADEVORGAENGE⠒ID
-- the other disjoint
when new.LADEVORGAENGE⠒ID
is null
and new.LADEVORGANG_PT
is not null then (select ID
from BUDGET_LOG.LADEVORGAENGE
where 1 = 1
and LADEVORGANG_PT = new.LADEVORGANG_PT
and 1 = 1)
-- check for conflicts
when new.LADEVORGAENGE⠒ID
is not null and new.LADEVORGANG_PT is not null then (select ID
from
BUDGET_LOG.LADEVORGAENGE
where 1 = 1
and ID =
new.LADEVORGAENGE⠒ID
and LADEVORGANG_PT =
new.LADEVORGANG_PT
and 1 = 1)
-- fallback
else null
end);
create or replace rule TARIF_PROGRESSIV_DENORM⠒UPD as
on update
to BUDGET.TARIF_PROGRESSIV_DENORM
do instead
update TARIF_PROGRESSIV
set KANTON⠒ID = case
-- a disjoint
when new.KANTONSCODE is not null and new.KANTONSNAME
is null then (select ID
from BUDGET_MASTER.KANTON
where 1 = 1
and CODE = new.KANTONSCODE
and 1 = 1)
-- the other disjoint
when new.KANTONSCODE is null and new.KANTONSNAME is
not null then (select ID
from BUDGET_MASTER.KANTON
where 1 = 1
and NAME = new.KANTONSNAME
and 1 = 1)
-- check for conflicts
when new.KANTONSCODE is not null and new.KANTONSNAME
is not null then (select ID
from BUDGET_MASTER.KANTON
where 1 = 1
and CODE = new.KANTONSCODE
and NAME = new.KANTONSNAME
and 1 = 1)
-- fallback
else null
end,
TARIFGRUPPE⠒ID = case
-- a disjoint
when new.TARIFGRUPPENCODE is not null and
new.TARIFGRUPPENBESCHREIBUNG is null then (select ID
from BUDGET_MASTER.TARIFGRUPPE
where 1 = 1
and CODE = new.TARIFGRUPPENCODE
and 1 = 1)
-- the other disjoint
when new.TARIFGRUPPENCODE is null and
new.TARIFGRUPPENBESCHREIBUNG is not null then (select ID
from BUDGET_MASTER.TARIFGRUPPE
where 1 = 1
and BESCHREIBUNG =
new.TARIFGRUPPENBESCHREIBUNG
and 1 = 1)
-- check for conflicts
when new.TARIFGRUPPENCODE is not null and
new.TARIFGRUPPENBESCHREIBUNG is not null then (select ID
from BUDGET_MASTER.TARIFGRUPPE
where 1 = 1
and CODE = new.TARIFGRUPPENCODE
and BESCHREIBUNG =
new.TARIFGRUPPENBESCHREIBUNG
and 1 = 1)
-- fallback
else null
end,
KINDER_ANZAHL = coalesce(new.KINDER_ANZAHL, old.KINDER_ANZAHL),
KIRCHENSTEUER_FLAG = coalesce(new.KIRCHENSTEUER_FLAG,
old.KIRCHENSTEUER_FLAG),
EINKOMMEN_STEUERBARES_RANGE =
coalesce(new.EINKOMMEN_STEUERBARES_RANGE, old.EINKOMMEN_STEUERBARES_RANGE),
TARIFSCHRITT = coalesce(new.TARIFSCHRITT, old.TARIFSCHRITT),
MINDESTSTEUER = coalesce(new.MINDESTSTEUER, old.MINDESTSTEUER),
STEUERSATZ = coalesce(new.STEUERSATZ, old.STEUERSATZ)
where id = old.ID;
create or replace rule TARIF_PROGRESSIV_DENORM⠒DEL as
on delete
to BUDGET.TARIF_PROGRESSIV_DENORM
do instead
delete
from TARIF_PROGRESSIV
where id = old.id;
commit;