Index: trunk/import/alkis-functions.sql
===================================================================
--- trunk/import/alkis-functions.sql	(revision 331)
+++ trunk/import/alkis-functions.sql	(revision 336)
@@ -22,4 +22,12 @@
 --               "alkis_mviews()", delete_feature_kill_vers07(), alkis_beziehung_inserted()
 --             Trigger-Function "delete_feature_hist" durch aktuelle Version aus OSGeo4W ersetzt.
+
+-- 2014-09-19  FJ: Korrektur "delete_feature_hist()":
+--             Ausgehend von Version: https://raw.githubusercontent.com/norBIT/alkisimport/master/alkis-functions.sql
+--             Bei der Erstabagabe mit Vollhistorie (ibR) werden mehrere ZwischenstÃ€nde von Objekten eingelesen.
+--             Einige davon wurden bereits mit "endet" ausgeliefert (in replace-SÃ€tzen).
+--             Wenn der Trigger ausgelÃ¶st wird (in einem zweiten Durchlauf von PostNAS) kann es jeweils
+--             mehrerer VorgÃ€nger- und Nachfolger-Objekte mit und ohne "endet IS NULL" geben.
+
 
 -- Table/View/Sequence lÃ¶schen, wenn vorhanden
@@ -281,5 +289,4 @@
 $$ LANGUAGE plpgsql;
 
--- Die folgende Trigger-Version kopiert aus  OSGeo4W / apps / alkis-import, Datei:alkis-functions.sql
 
 -- Im Trigger 'delete_feature_trigger' muss eine dieser beiden Funktionen
@@ -287,69 +294,138 @@
 -- aktuelle oder auch historische Objekte in der Datenbank gefÃŒhrt werden sollen.
 
+
+/*	Beschreibung und Umfeld des "delete_feature_trigger":
+	-----------------------------------------------------
+
+Der Konverter "ogr2ogr", in den PostNAS als Eingabe-Modul fÃŒr das NAS-Format eingebettet ist, 
+ist vom Wesen her eigentlich ein 1:1-Konverter.
+Ãblicherweise liest er ein Eingabe-GIS-Format, analysiert es und erzeugt dann die gleiche Struktur in 
+einem Ausgabe-GIS-Format.
+Das ALKIS-Format "NAS" als einmaliger Datenauszug (enthÃ€lt nur Funktion "insert") kÃ¶nnte in diesem 
+Rahmen vielleicht auch ohne Trigger umgesetzt werden.
+
+Viel komplexer umzusetzen sind jedoch solche NAS-Daten, die im Rahmen des NBA-Verfahrens von ALKIS abgegeben werden.
+		NBA  =  Nutzerbezogene Bestandsdaten-Aktualisierung.
+In einem NBA-Verfahren wird eine primÃ€re Datenquelle (ALKIS) mit einem SekundÃ€rbestand (PostGIS) synchronisiert.
+Es erfolgt zunÃ€chst eine Erstabgabe fÃŒr einen definierten Zeitpunkt.
+SpÃ€ter gibt es Aktualisierungen auf weitere Zeitpunkte. Die dazu ÃŒbermittelten Differenzdaten enthalten
+nicht nur reine Daten (INSERT) sondern auch Anweisungen zur Ãnderung der zu frÃŒheren Zeitpunkten ÃŒbermittelten Daten.
+Diese Ãnderungs-Anweisungen werden innerhalb des Konverters ogr2ogr nicht komplett verarbeitet.
+Er verarbeitet zunÃ€chst nur die enthaltenen Datenfelder zum Objekt, die er in neue Zeilen in die Objekt-Tabellen einstellt.
+
+AnschlieÃend werden Informationen zum ObjektschlÃŒssel (gml_id) und zur Lebensdauer des Objektes (beginnt) zusammen 
+mit der Operation (Kontext = "delete", "update" oder "replace") in die Tabelle "delete" eingetragen.
+Dieser Eintrag in "delete" lÃ¶st den Trigger aus, der sich dann darum kÃŒmmert, LÃ¶schungen oder 
+Ãnderungen an VorgÃ€ngerversionen vorzunehmen.
+
+Im NBA-Verfahren sind verschiedene "Abgabearten" mÃ¶glich:
+Die Abgabeart 1000 hat zum Ziel, im SekundÃ€rbestand jeweils den letzten aktuellen Stand bereit zu stellen.
+Die Abgabeart 3100 hat zum Ziel, im SekundÃ€rbestand eine komplette Historie bereit zu stellen, die auch alle
+ZwischenstÃ€nde enthÃ€lt. Ein nicht mehr gÃŒltiges Objekt wird dann mit einem Eintrag in "endet" deaktiviert, 
+verbleibt aber in der Datenbank. Bei der Abgabeart 3100 sind bereits in der Erstabgabe Aktualisierungs-
+Funktionen (delete, update, replace) enthalten weil mehrere historische Versionen von Objekten geliefert werden.
+
+Eine NBA-Lieferung fÃŒr ein Gebiet ist in mehrere Dateien aufgeteilt, die nacheinander abgearbeitet werden.
+Erst mit der Verarbeitung der letzten Datei einer Lieferung ist die Datenbank wieder konsistent und zeigt den
+Zustand zum neuen Abgabezeitpunkt.
+
+Jede dieser NAS-Dateien wird von PostNAS in mehreren DurchlÃ€ufen vearbeitet.
+1. Im ersten Durchlauf wird die 1:1-Konvertierung der Daten vorgenommen. 
+   Die Feldinhalte der NAS-Datei werden in neue Zeilen in die Objekttabellen der Datenbank ÃŒbertragen. 
+2. Dann werden in einem weiteren Durchlauf die Operationen "delete", "update" und "replace" verarbeitet.
+   Diese werden von PostNAS in die Tabelle "delete" eingetragen, dies lÃ¶st den Trigger aus.
+	
+FÃŒr die Arbeitsweise des Triggers bedeutet das:
+
+An dem Zeitpunkt, an dem der Trigger ausgelÃ¶st wird, stehen bereits alle Daten zu den Objekten in den Objekt-Tabellen.
+Darin ist aber mÃ¶glicherweise das Feld "endet" noch nicht gefÃŒllt. 
+
+WÃ€hrend der Konvertierung der Erstabgabe einer NBA-Abgabe der Abgabeart 3100 kÃ¶nnen aber Objekte doch schon beendet sein.
+Bei einer Erstabgabe der Abgabeart 3100 kÃ¶nnen mehrere Generation des selben Objektes vorhanden sein, 
+die alle in der gleichen NAS-Datei geliefert wurden. 
+Das Feld "endet" ist dann nicht geeignet zu entscheiden, welches die letzte (aktuelle) Version ist.
+
+Es kann vorkommen, dass Zwischenversionen in der Objekt-Tabelle bereits beendet sind weil sie direkt mit ihrem 
+Endet-Datum geliefert wurden. Dieses wurde bereits beim ersten Durchlauf von ogr2ogr wie ein normales Datenfeld eingetragen.
+In Beispieldaten wurde analysiert, dass ein bereits beendetes Objekt in einem "insert" kein "endet" mitbringt. 
+Dies muss vom Trigger beendet werden, wenn dieser einen replace fÃŒr den Nachfolger bekommt.
+
+Im gleichen Bestand wurden jedoch Nachfolger gefunden die mit einem "replace"-Satz gekommen sind 
+und bereits beendet waren, weil sie ihrerseits wieder Nachfolger hatten. 
+
+Das jeweils folgende "replace" kann also ein VorgÃ€nger-Objekt mit oder ohne "endet"-Eintrag vorfinden.
+Es kÃ¶nnen auch sowohl VorgÃ€nger- als auch bereits Nachfolger-Versionen eines Objektes vorhanden sein, wenn der Trigger
+ausgelÃ¶st wird.
+
+Aufgabe des Triggers ist es, zu einem verÃ€nderten Objekt jeweils den unmittelbaren VorgÃ€nger zu ermitteln
+und - falls noch nicht geschehen - den passenden endet-Eintrag nachzutragen.
+Wenn in den Daten kein "endet" mitgeliefert wird, dann wird der Beginn der Folge-Version des Objektes verwendet
+um den VorgÃ€nger zu beenden.
+
+Wenn ein Objekt bereits mit endet-Datum geliefert wurde, dann wird dies zwar in die Obkjekt-Tabelle eingetragen,
+der endet-Eintrag in dem replace-Satz in der delete-Tabelle, der den Trigger auslÃ¶st, ist trotzdem leer.
+Es ist ÃŒberlegen, ob dies im PostNAS-Programm geÃ€ndert werden sollte.
+
+Aufgrund der KomplexitÃ€t dieser Mechanismen ist davon auszugehen, dass es Hersteller-spezifische Unterschiede
+gibt und auch Unterschiede zuwischen verschiedenen Versions-StÃ€nden des selben Herstellers.
+Die Arbeitsweise des Triggers muss daher regelmÃ€Ãig ÃŒberprÃŒft werden.
+
+*/
+
+-- Achtung: FÃŒr diese Trigger-Version mÃŒssen die SchlÃŒsselfelder "gml_id" in allen Tabellen 
+--          wieder auf 16 Stellen fix gekÃŒrzt werden!
+
 -- LÃ¶schsatz verarbeiten (MIT Historie)
--- context='update'        => "endet" auf ÃŒbergebene Zeit setzen und anlass festhalten
 -- context='delete'        => "endet" auf aktuelle Zeit setzen
 -- context='replace'       => "endet" des ersetzten auf "beginnt" des neuen Objekts setzen
+-- context='update'        => "endet" auf ÃŒbergebene Zeit setzen und "anlass" festhalten
 CREATE OR REPLACE FUNCTION delete_feature_hist() RETURNS TRIGGER AS $$
 DECLARE
+	n INTEGER;
+	vbeginnt TEXT;
+	replgml TEXT;
+	featgml TEXT;
 	s TEXT;
-	alt_id TEXT;
-	neu_id TEXT;
-	beginnt TEXT;
-	endete TEXT;
-	n INTEGER;
-BEGIN
-	NEW.context := lower(NEW.context);
-	IF NEW.context IS NULL THEN
-		NEW.context := 'delete';
-	END IF;
-
-	-- TIMESTAMP weder in gml_id noch identifier verlÃ€Ãlich.
-	-- also ggf. aus Datenbank holen
+BEGIN
+	NEW.context := coalesce(lower(NEW.context),'delete');
+
+	IF NEW.anlass IS NULL THEN
+		NEW.anlass := '';
+	END IF;
+	featgml := substr(NEW.featureid, 1, 16); -- gml_id ohne Timestamp
 
 	IF length(NEW.featureid)=32 THEN
-		alt_id  := substr(NEW.featureid, 1, 16);
-
-		IF NEW.featureid<>NEW.replacedBy THEN
-			-- Beginnt-Datum aus Timestamp
-			beginnt := substr(NEW.featureid, 17, 4) || '-'
-				|| substr(NEW.featureid, 21, 2) || '-'
-				|| substr(NEW.featureid, 23, 2) || 'T'
-				|| substr(NEW.featureid, 26, 2) || ':'
-				|| substr(NEW.featureid, 28, 2) || ':'
-				|| substr(NEW.featureid, 30, 2) || 'Z'
-				;
-		END IF;
+		-- beginnt-Zeit der zu ersetzenden Vorgaenger-Version des Objektes
+		vbeginnt := substr(NEW.featureid, 17, 4) || '-'
+			|| substr(NEW.featureid, 21, 2) || '-'
+			|| substr(NEW.featureid, 23, 2) || 'T'
+			|| substr(NEW.featureid, 26, 2) || ':'
+			|| substr(NEW.featureid, 28, 2) || ':'
+			|| substr(NEW.featureid, 30, 2) || 'Z' ;
 	ELSIF length(NEW.featureid)=16 THEN
-		alt_id  := NEW.featureid;
+		-- Ãltestes nicht gelÃ¶schtes Objekt
+		EXECUTE 'SELECT min(beginnt) FROM ' || NEW.typename
+		        || ' WHERE gml_id=''' || featgml || ''''
+		        || ' AND endet IS NULL'
+			INTO vbeginnt;
+
+		IF vbeginnt IS NULL THEN
+			RAISE EXCEPTION '%: Keinen Kandidaten zum LÃ¶schen gefunden.', NEW.featureid;
+		END IF;
 	ELSE
-		RAISE EXCEPTION '%: LÃ€nge 16 oder 32 statt % erwartet.', NEW.featureid, length(NEW.featureid);
-	END IF;
-
-	IF beginnt IS NULL THEN
-		-- Beginnt-Datum des Ã€ltesten Eintrag, der nicht untergegangen ist
-		-- => der Satz dessen 'endet' gesetzt werden muÃ
-		EXECUTE 'SELECT min(beginnt) FROM ' || NEW.typename
-			|| ' WHERE gml_id=''' || alt_id || ''''
-			|| ' AND endet IS NULL'
-			INTO beginnt;
-	END IF;
-
-	IF beginnt IS NULL THEN
-		IF NEW.context = 'delete' OR NEW.safetoignore = 'true' THEN
-			RAISE NOTICE 'Kein Beginndatum fÃŒr Objekt % gefunden - ignoriert.', alt_id;
-			NEW.ignored := true;
-			RETURN NEW;
-		ELSE
-			RAISE EXCEPTION 'Kein Beginndatum fÃŒr Objekt % gefunden.', alt_id;
-		END IF;
+		RAISE EXCEPTION '%: Identifikator gescheitert.', NEW.featureid;
 	END IF;
 
 	IF NEW.context='delete' THEN
-		endete := to_char(CURRENT_TIMESTAMP AT TIME ZONE 'UTC','YYYY-MM-DD"T"HH24:MI:SS"Z"');
+		NEW.endet := to_char(CURRENT_TIMESTAMP AT TIME ZONE 'UTC','YYYY-MM-DD"T"HH24:MI:SS"Z"');
+
+	ELSIF NEW.context='update' THEN
+		IF NEW.endet IS NULL THEN
+			RAISE EXCEPTION '%: Endedatum nicht gesetzt', NEW.featureid;
+		END IF;
 
 	ELSIF NEW.context='replace' THEN
 		NEW.safetoignore := lower(NEW.safetoignore);
-
+		replgml := substr(NEW.replacedby, 1, 16); -- ReplcedBy gml_id ohne Timestamp
 		IF NEW.safetoignore IS NULL THEN
 			RAISE EXCEPTION '%: safeToIgnore nicht gesetzt.', NEW.featureid;
@@ -358,97 +434,65 @@
 		END IF;
 
-		IF length(NEW.replacedBy)=32 THEN
-			-- Beginnt-Datum aus Timestamp
-			IF NEW.featureid<>NEW.replacedBy THEN
-				endete  := substr(NEW.replacedBy, 17, 4) || '-'
-					|| substr(NEW.replacedBy, 21, 2) || '-'
-					|| substr(NEW.replacedBy, 23, 2) || 'T'
-					|| substr(NEW.replacedBy, 26, 2) || ':'
-					|| substr(NEW.replacedBy, 28, 2) || ':'
-					|| substr(NEW.replacedBy, 30, 2) || 'Z'
-					;
+		IF length(NEW.replacedby)=32 AND NEW.replacedby<>NEW.featureid THEN
+			NEW.endet := substr(NEW.replacedby, 17, 4) || '-'
+				|| substr(NEW.replacedby, 21, 2) || '-'
+				|| substr(NEW.replacedby, 23, 2) || 'T'
+				|| substr(NEW.replacedby, 26, 2) || ':'
+				|| substr(NEW.replacedby, 28, 2) || ':'
+				|| substr(NEW.replacedby, 30, 2) || 'Z' ;
+		END IF;
+
+		-- Satz-Paarung VorgÃ€nger-Nachfolger in der Objekttabelle suchen.
+		-- Der VorgÃ€nger muss noch beendet werden. Der Nachfolger kann bereits beendet sein.
+		-- Das "beginn" des Nachfolgers anschlieÃend als "endet" des Vorgaengers verwenden.
+		-- Normalfall bei NBA-Aktualisierungslaeufen. v=VorgÃ€nger, n=Nachfolger.
+		IF NEW.endet IS NULL THEN
+			EXECUTE 'SELECT min(n.beginnt) FROM ' || NEW.typename || ' n'
+				|| ' JOIN ' || NEW.typename || ' v ON v.ogc_fid<n.ogc_fid'
+				|| ' WHERE v.gml_id=''' || featgml
+				|| ''' AND n.gml_id=''' || replgml
+				|| ''' AND v.endet IS NULL'
+				INTO NEW.endet;
+		--	RAISE NOTICE 'endet setzen fuer Vorgaenger % ', NEW.endet;
+		END IF;
+
+		-- Satz-Paarung VorgÃ€nger-Nachfolger in der Objekttabelle suchen.
+		-- Der VorgÃ€nger ist bereits beendet worden weil "endet" in den Daten gefÃŒllt war.
+		-- Dieser Fall kommt bei der Erstabgabe mit Vollhistorie vor.
+		IF NEW.endet IS NULL THEN
+			EXECUTE 'SELECT min(n.beginnt) FROM ' || NEW.typename || ' n'
+				|| ' JOIN ' || NEW.typename || ' v ON v.endet=n.beginnt '
+				|| ' WHERE v.gml_id=''' || featgml
+				|| ''' AND n.gml_id=''' || replgml
+				|| ''' AND v.beginnt=''' || vbeginnt || ''''
+				INTO NEW.endet;
+
+			IF NOT NEW.endet IS NULL THEN
+			--	RAISE NOTICE '%: Vorgaenger ist schon endet', NEW.featureid;
+				NEW.ignored=false;
+				RETURN NEW;
 			END IF;
-		ELSIF length(NEW.replacedBy)<>16 THEN
-			RAISE EXCEPTION '%: LÃ€nge 16 oder 32 statt % erwartet.', NEW.replacedBy, length(NEW.replacedBy);
-		END IF;
-
-		neu_id := NEW.replacedBy;
-		IF endete IS NULL THEN
-			-- Beginnt-Datum des neuesten Eintrag, der nicht untergegangen ist
-			-- => Enddatum fÃŒr vorherigen Satz
-			EXECUTE 'SELECT max(beginnt) FROM ' || NEW.typename
-				|| ' WHERE gml_id=''' || NEW.replacedBy || ''''
-				|| ' AND beginnt>''' || beginnt || ''''
-				|| ' AND endet IS NULL'
-				INTO endete;
-			IF endete IS NULL AND length(NEW.replacedBy)=32 THEN
-				EXECUTE 'SELECT max(beginnt) FROM ' || NEW.typename
-					|| ' WHERE gml_id=''' || substr(NEW.replacedBy, 1, 16) || ''''
-					|| ' AND beginnt>''' || beginnt || ''''
-					|| ' AND endet IS NULL'
-				INTO endete;
-				neu_id := substr(NEW.replacedBy, 1, 16);
+		END IF;
+
+		IF NEW.endet IS NULL THEN -- "endet" fÃŒr den VorgÃ€nger konnte nicht ermittelt werden
+			IF NEW.safetoignore='false' THEN
+				RAISE EXCEPTION '%: Beginn des ersetzenden Objekts % nicht gefunden.', NEW.featureid, NEW.replacedby;
 			END IF;
-		END IF;
-
-		IF alt_id<>substr(neu_id, 1, 16) THEN
-			RAISE NOTICE 'Objekt % wird durch Objekt % ersetzt.', alt_id, neu_id;
-		END IF;
-
-		IF endete IS NULL THEN
-			RAISE NOTICE 'Kein Beginndatum fÃŒr Objekt % gefunden.', NEW.replacedBy;
-		END IF;
-
-		IF endete IS NULL OR beginnt=endete THEN
-			RAISE EXCEPTION 'Objekt % wird durch Objekt % ersetzt (leere Lebensdauer?).', alt_id, neu_id;
-		END IF;
-	ELSIF NEW.context='update' THEN
-		endete := NEW.endet;
+			NEW.ignored=true;
+			RETURN NEW;
+		END IF; 
 	ELSE
 		RAISE EXCEPTION '%: UngÃŒltiger Kontext % (''delete'', ''replace'' oder ''update'' erwartet).', NEW.featureid, NEW.context;
 	END IF;
 
-	s   := 'UPDATE ' || NEW.typename
-	    || ' SET endet=''' || endete || ''''
-	    || ',anlass=''' || coalesce(NEW.anlass,'000000') || ''''
-	    || ' WHERE gml_id=''' || NEW.featureid || ''''
-	    || ' AND beginnt=''' || beginnt || ''''
-	    || ' AND endet IS NULL';
+	-- Vorgaenger ALKIS-Objekt nun beenden
+	s := 'UPDATE ' || NEW.typename
+	  || ' SET endet=''' || NEW.endet || ''' ,anlass=''' || NEW.anlass || ''''
+	  || ' WHERE gml_id=''' || featgml || ''' AND beginnt=''' || vbeginnt || '''' ;
 	EXECUTE s;
 	GET DIAGNOSTICS n = ROW_COUNT;
-	IF n=0 AND alt_id<>NEW.featureid THEN
-		s   := 'UPDATE ' || NEW.typename
-		    || ' SET endet=''' || endete || ''''
-	            || ',anlass=''' || coalesce(NEW.anlass,'000000') || ''''
-	            || ' WHERE gml_id=''' || alt_id || ''''
-	            || ' AND beginnt=''' || beginnt || ''''
-	            || ' AND endet IS NULL';
-		EXECUTE s;
-		GET DIAGNOSTICS n = ROW_COUNT;
-	END IF;
-
+	-- RAISE NOTICE 'SQL[%]:%', n, s;
 	IF n<>1 THEN
-		RAISE NOTICE 'SQL[%<>1]: %', n, s;
-		IF NEW.context = 'delete' OR NEW.safetoignore = 'true' THEN
-			RAISE NOTICE '%: Untergangsdatum von % Objekten statt einem auf % gesetzt - ignoriert', NEW.featureid, n, endete;
-			NEW.ignored := true;
-			RETURN NEW;
-		ELSIF n=0 THEN
-			EXECUTE 'SELECT endet FROM ' || NEW.typename ||
-				' WHERE gml_id=''' || alt_id || '''' ||
-				' AND beginnt=''' || beginnt || ''''
-				INTO endete;
-
-			IF NOT endete IS NULL THEN
-				RAISE NOTICE '%: Objekt bereits % untergegangen - ignoriert', NEW.featureid, endete;
-			ELSE
-				RAISE NOTICE '%: Objekt nicht gefunden - ignoriert', NEW.featureid;
-			END IF;
-
-			NEW.ignored := true;
-			RETURN NEW;
-		ELSE
-			RAISE EXCEPTION '%: Untergangsdatum von % Objekten statt einem auf % gesetzt - Abbruch', NEW.featureid, n, endete;
-		END IF;
+		RAISE EXCEPTION '%: % schlug fehl [%]', NEW.featureid, NEW.context, n;
 	END IF;
 
@@ -460,4 +504,10 @@
 
 -- 2014-09-04: Version 0.8 ohne "alkis_beziehungen"-Tabelle
+-- Unterschied von "delete_feature_kill" zur Version "delete_feature_hist":
+--  Historisch gewordene Objekte werden nicht auf "endet" gesetzt sondern ganz aus der Datenbank gelÃ¶scht.
+-- Achtung: Wenn diese Funktion mit dem "delete_feature_trigger" der Tabelle "delete" verknÃŒpft ist,
+-- dann dÃŒrfen nur NAS-NBA-Daten verarbeitet werden, die mit der Abgabeart 1000 erzeugt wurden.
+-- Wenn Daten der Abgabeart 3100 verarbeitet werden kommen update-Anweisungen in den Daten vor, 
+-- die dieser Trigger nicht verarbeiten kann.
 CREATE OR REPLACE FUNCTION delete_feature_kill() RETURNS TRIGGER AS $$
 DECLARE
@@ -492,15 +542,4 @@
 END;
 $$ LANGUAGE plpgsql;
-
-
--- BeziehungssÃ€tze aufrÃ€umen
-/*
-CREATE OR REPLACE FUNCTION alkis_beziehung_inserted() RETURNS TRIGGER AS $$
-BEGIN
-	DELETE FROM alkis_beziehungen WHERE ogc_fid<NEW.ogc_fid AND beziehung_von=NEW.beziehung_von AND beziehungsart=NEW.beziehungsart AND beziehung_zu=NEW.beziehung_zu;
-	RETURN NEW;
-END;
-$$ LANGUAGE plpgsql;
-*/
 
 
