wiki/mandates/pek/datenmodell/migration_001_initial_schema.sql
ValueOn AG 15f0e51bd0 pek
2025-10-24 21:42:47 +02:00

393 lines
14 KiB
PL/PgSQL

-- ============================================================================
-- Architektur-Planungs-App Datenbank Schema
-- PostgreSQL 15+ mit PostGIS 3.4+
-- Schweizer Koordinatensystem: LV95 (EPSG:2056)
-- ============================================================================
-- PostGIS Extension aktivieren
CREATE EXTENSION IF NOT EXISTS postgis;
-- UUID Extension für uuid_generate_v4()
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- ============================================================================
-- ENUMS
-- ============================================================================
CREATE TYPE status_prozess AS ENUM (
'Eingang',
'Analyse',
'Studie',
'Planung',
'Baurechtsverfahren',
'Umsetzung',
'Archiv'
);
CREATE TYPE dokument_typ AS ENUM (
'Datei',
'Url'
);
CREATE TYPE tag_typ AS ENUM (
'Kataster Objekte',
'Kataster Werkeleitungen',
'Kataster Belastete Standorte',
'Kataster Bäume',
'Zonenplan',
'Planungs- und Baugesetz (PGB)',
'Bau- und Zonenordnung (BZO)',
'Parkplatzverordnung',
'Eigentümerauskunft',
'Grundbuchauszug'
);
CREATE TYPE geo_tag_typ AS ENUM (
'Referenzpunkt Kat. 1',
'Referenzpunkt Kat. 2',
'Referenzpunkt Kat. 3',
'Geometeraufnahme'
);
CREATE TYPE ja_nein AS ENUM (
'',
'Ja',
'Nein'
);
-- ============================================================================
-- HAUPTTABELLEN
-- ============================================================================
-- Land
CREATE TABLE land (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
label VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Kanton
CREATE TABLE kanton (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
label VARCHAR(255) NOT NULL,
baureglement_aktuell_id UUID,
baureglement_revision_id UUID,
bauverordnung_aktuell_id UUID,
bauverordnung_revision_id UUID,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Gemeinde
CREATE TABLE gemeinde (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
label VARCHAR(255) NOT NULL,
plz VARCHAR(10),
bzo_aktuell_id UUID,
bzo_revision_id UUID,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Dokument
CREATE TABLE dokument (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
label VARCHAR(255) NOT NULL,
versionsbezeichnung VARCHAR(100),
typ dokument_typ NOT NULL,
format VARCHAR(50),
dokument_referenz TEXT NOT NULL,
tags tag_typ[],
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Projekt
CREATE TABLE projekt (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
label VARCHAR(255) NOT NULL,
status_prozess status_prozess[],
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Parzelle
CREATE TABLE parzelle (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
label VARCHAR(255) NOT NULL,
parzellen_nummern VARCHAR(50)[],
eigentuemerschaaft VARCHAR(255),
strasse_nr VARCHAR(255),
-- Geografischer Kontext
land_id UUID REFERENCES land(id),
kanton_id UUID REFERENCES kanton(id),
gemeinde_id UUID REFERENCES gemeinde(id),
-- Geometrie (PostGIS)
geo_umfang GEOMETRY(POLYGON, 2056),
-- Bauliche Parameter
bauzone VARCHAR(50),
az DECIMAL(5,2),
bz DECIMAL(5,2),
vollgeschoss_zahl INTEGER,
anrechenbar_dachgeschoss DECIMAL(3,2),
anrechenbar_untergeschoss DECIMAL(3,2),
gebaeudehoehe_max DECIMAL(6,2),
-- Regelungen
regeln_grenzabstand TEXT,
regeln_mehrlaengenzuschlag TEXT,
regeln_mehrhoehenzuschlag TEXT,
-- Schutzzonen
hochwasserschutzzone VARCHAR(100),
laermschutzzone VARCHAR(100),
grundwasserschutzzone VARCHAR(100),
-- Eigenschaften
parzelle_bebaut ja_nein,
parzelle_erschlossen ja_nein,
hanglage ja_nein,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- GeoPunkt
CREATE TABLE geo_punkt (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
x DECIMAL(12,3) NOT NULL, -- LV95 Ostwert
y DECIMAL(12,3) NOT NULL, -- LV95 Nordwert
z DECIMAL(8,3), -- Höhe über Meer
referenzen geo_tag_typ[],
projekt_id UUID REFERENCES projekt(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Kontext (Polymorphe Beziehung)
CREATE TABLE kontext (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
thema VARCHAR(255) NOT NULL,
inhalt TEXT NOT NULL,
-- Polymorphe Foreign Keys
projekt_id UUID REFERENCES projekt(id) ON DELETE CASCADE,
parzelle_id UUID REFERENCES parzelle(id) ON DELETE CASCADE,
land_id UUID REFERENCES land(id) ON DELETE CASCADE,
kanton_id UUID REFERENCES kanton(id) ON DELETE CASCADE,
gemeinde_id UUID REFERENCES gemeinde(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-- Constraint: Kontext muss zu genau einer Entität gehören
CONSTRAINT kontext_single_parent CHECK (
(projekt_id IS NOT NULL)::INTEGER +
(parzelle_id IS NOT NULL)::INTEGER +
(land_id IS NOT NULL)::INTEGER +
(kanton_id IS NOT NULL)::INTEGER +
(gemeinde_id IS NOT NULL)::INTEGER = 1
)
);
-- ============================================================================
-- JUNCTION TABLES (Many-to-Many Beziehungen)
-- ============================================================================
-- Projekt <-> Parzelle
CREATE TABLE projekt_parzelle (
projekt_id UUID NOT NULL REFERENCES projekt(id) ON DELETE CASCADE,
parzelle_id UUID NOT NULL REFERENCES parzelle(id) ON DELETE CASCADE,
PRIMARY KEY (projekt_id, parzelle_id)
);
-- Projekt <-> Dokument (Bauherrschaft)
CREATE TABLE projekt_dokument_bauherrschaft (
projekt_id UUID NOT NULL REFERENCES projekt(id) ON DELETE CASCADE,
dokument_id UUID NOT NULL REFERENCES dokument(id) ON DELETE CASCADE,
PRIMARY KEY (projekt_id, dokument_id)
);
-- Projekt <-> Dokument (Planung)
CREATE TABLE projekt_dokument_planung (
projekt_id UUID NOT NULL REFERENCES projekt(id) ON DELETE CASCADE,
dokument_id UUID NOT NULL REFERENCES dokument(id) ON DELETE CASCADE,
PRIMARY KEY (projekt_id, dokument_id)
);
-- Parzelle <-> Parzelle (Nachbarn)
CREATE TABLE parzelle_nachbar (
parzelle_id UUID NOT NULL REFERENCES parzelle(id) ON DELETE CASCADE,
nachbar_id UUID NOT NULL REFERENCES parzelle(id) ON DELETE CASCADE,
PRIMARY KEY (parzelle_id, nachbar_id),
CHECK (parzelle_id != nachbar_id) -- Parzelle kann nicht ihr eigener Nachbar sein
);
-- Parzelle <-> Dokument
CREATE TABLE parzelle_dokument (
parzelle_id UUID NOT NULL REFERENCES parzelle(id) ON DELETE CASCADE,
dokument_id UUID NOT NULL REFERENCES dokument(id) ON DELETE CASCADE,
PRIMARY KEY (parzelle_id, dokument_id)
);
-- ============================================================================
-- FOREIGN KEY CONSTRAINTS (nachträglich für Kanton/Gemeinde Dokumente)
-- ============================================================================
ALTER TABLE kanton
ADD CONSTRAINT fk_kanton_baureglement_aktuell
FOREIGN KEY (baureglement_aktuell_id) REFERENCES dokument(id),
ADD CONSTRAINT fk_kanton_baureglement_revision
FOREIGN KEY (baureglement_revision_id) REFERENCES dokument(id),
ADD CONSTRAINT fk_kanton_bauverordnung_aktuell
FOREIGN KEY (bauverordnung_aktuell_id) REFERENCES dokument(id),
ADD CONSTRAINT fk_kanton_bauverordnung_revision
FOREIGN KEY (bauverordnung_revision_id) REFERENCES dokument(id);
ALTER TABLE gemeinde
ADD CONSTRAINT fk_gemeinde_bzo_aktuell
FOREIGN KEY (bzo_aktuell_id) REFERENCES dokument(id),
ADD CONSTRAINT fk_gemeinde_bzo_revision
FOREIGN KEY (bzo_revision_id) REFERENCES dokument(id);
-- ============================================================================
-- INDICES für Performance
-- ============================================================================
-- Projekt Indices
CREATE INDEX idx_projekt_status ON projekt USING GIN (status_prozess);
-- Parzelle Indices
CREATE INDEX idx_parzelle_land ON parzelle(land_id);
CREATE INDEX idx_parzelle_kanton ON parzelle(kanton_id);
CREATE INDEX idx_parzelle_gemeinde ON parzelle(gemeinde_id);
CREATE INDEX idx_parzelle_bauzone ON parzelle(bauzone);
CREATE INDEX idx_parzelle_geo_umfang ON parzelle USING GIST(geo_umfang);
-- Dokument Indices
CREATE INDEX idx_dokument_typ ON dokument(typ);
CREATE INDEX idx_dokument_tags ON dokument USING GIN (tags);
-- GeoPunkt Indices
CREATE INDEX idx_geopunkt_projekt ON geo_punkt(projekt_id);
CREATE INDEX idx_geopunkt_referenzen ON geo_punkt USING GIN (referenzen);
-- Kontext Indices
CREATE INDEX idx_kontext_projekt ON kontext(projekt_id);
CREATE INDEX idx_kontext_parzelle ON kontext(parzelle_id);
CREATE INDEX idx_kontext_land ON kontext(land_id);
CREATE INDEX idx_kontext_kanton ON kontext(kanton_id);
CREATE INDEX idx_kontext_gemeinde ON kontext(gemeinde_id);
CREATE INDEX idx_kontext_thema ON kontext(thema);
-- ============================================================================
-- TRIGGER für updated_at
-- ============================================================================
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_land_updated_at BEFORE UPDATE ON land
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_kanton_updated_at BEFORE UPDATE ON kanton
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_gemeinde_updated_at BEFORE UPDATE ON gemeinde
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_dokument_updated_at BEFORE UPDATE ON dokument
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_projekt_updated_at BEFORE UPDATE ON projekt
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_parzelle_updated_at BEFORE UPDATE ON parzelle
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_geo_punkt_updated_at BEFORE UPDATE ON geo_punkt
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_kontext_updated_at BEFORE UPDATE ON kontext
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================================
-- VIEWS für häufige Abfragen
-- ============================================================================
-- View: Parzellen mit vollständigem geografischem Kontext
CREATE VIEW v_parzelle_vollstaendig AS
SELECT
p.*,
l.label as land_name,
k.label as kanton_name,
g.label as gemeinde_name,
g.plz as gemeinde_plz,
ST_AsGeoJSON(p.geo_umfang) as geo_umfang_geojson,
ST_Area(p.geo_umfang) as flaeche_m2
FROM parzelle p
LEFT JOIN land l ON p.land_id = l.id
LEFT JOIN kanton k ON p.kanton_id = k.id
LEFT JOIN gemeinde g ON p.gemeinde_id = g.id;
-- View: Projekte mit Perimeter-Information
CREATE VIEW v_projekt_mit_perimeter AS
SELECT
pr.id,
pr.label,
pr.status_prozess,
COUNT(DISTINCT pp.parzelle_id) as anzahl_parzellen,
STRING_AGG(DISTINCT pa.label, ', ') as parzellen_labels
FROM projekt pr
LEFT JOIN projekt_parzelle pp ON pr.id = pp.projekt_id
LEFT JOIN parzelle pa ON pp.parzelle_id = pa.id
GROUP BY pr.id, pr.label, pr.status_prozess;
-- ============================================================================
-- BEISPIELDATEN
-- ============================================================================
-- Land Schweiz
INSERT INTO land (label) VALUES ('Schweiz');
-- Kantone (Beispiele)
INSERT INTO kanton (label) VALUES
('Zürich'),
('Bern'),
('Luzern');
-- Gemeinden (Beispiele für Zürich)
INSERT INTO gemeinde (label, plz) VALUES
('Zürich', '8000'),
('Winterthur', '8400'),
('Uster', '8610');
-- ============================================================================
-- KOMMENTARE
-- ============================================================================
COMMENT ON TABLE projekt IS 'Bauprojekte mit Status und Perimeter';
COMMENT ON TABLE parzelle IS 'Grundstücke mit baulichen und rechtlichen Eigenschaften';
COMMENT ON TABLE dokument IS 'Dokumente und URLs mit Versionierung';
COMMENT ON TABLE geo_punkt IS '3D-Punkte im LV95-Koordinatensystem';
COMMENT ON TABLE kontext IS 'Flexible Kontextinformationen für verschiedene Entitäten';
COMMENT ON COLUMN parzelle.geo_umfang IS 'Parzellengrenze als PostGIS Polygon im LV95 (EPSG:2056)';
COMMENT ON COLUMN parzelle.az IS 'Ausnützungsziffer';
COMMENT ON COLUMN parzelle.bz IS 'Bebauungsziffer';
COMMENT ON COLUMN geo_punkt.x IS 'LV95 Ostwert (E), typisch 2480000-2840000';
COMMENT ON COLUMN geo_punkt.y IS 'LV95 Nordwert (N), typisch 1070000-1300000';
COMMENT ON COLUMN geo_punkt.z IS 'Höhe über Meer in Metern';
-- ============================================================================
-- Ende der Migration
-- ============================================================================