-- ============================================================================ -- 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 -- ============================================================================