# HG changeset patch # User Tom Gottfried # Date 1570200004 -7200 # Node ID d2eac69ba86bb9d8a4282a8c5891bd98fe84fa68 # Parent f77a6f9216aedf30811ba62aea15c3a51d40eb45# Parent 7caf620dda50ebf9b5adc33832ccd8d03855a525 Merge default into geoserver_sql_views diff -r 7caf620dda50 -r d2eac69ba86b pkg/geoserver/boot.go --- a/pkg/geoserver/boot.go Fri Oct 04 11:58:37 2019 +0200 +++ b/pkg/geoserver/boot.go Fri Oct 04 16:40:04 2019 +0200 @@ -34,7 +34,6 @@ const ( workspaceName = "gemma" datastoreName = "gemma" - databaseScheme = "waterway" databaseType = "postgis" primaryKeyMetadataTbl = "waterway.gt_pk_metadata" ) @@ -60,6 +59,42 @@ return bytes.NewReader(buf.Bytes()) } +// XXX: Creating SQL views with JSON via GeoServer REST-API fails +// Begin code for handling with XML instead +func toXMLStream(x interface{}) io.Reader { + buf := bytes.NewBufferString(xml.Header) + if err := xml.NewEncoder(buf).Encode(x); err != nil { + // Should not happen + log.Printf("warn: bad XML: %v\n", err) + } + return bytes.NewReader(buf.Bytes()) +} + +type ftXML struct { + XMLName xml.Name `xml:"featureType"` + Name string `xml:"name"` + Title string `xml:"title"` + SRS *string `xml:"srs,omitempty"` + Metadata ftMetadata +} + +type ftMetadata struct { + XMLName xml.Name `xml:"metadata"` + Entry ftMetadataEntry +} + +type ftMetadataEntry struct { + XMLName xml.Name `xml:"entry"` + Key string `xml:"key,attr"` + VirtTable ftVirtTable +} + +type ftVirtTable struct { + XMLName xml.Name `xml:"virtualTable"` + Name string `xml:"name"` + SQL string `xml:"sql"` +} // End code for handling with XML + func asJSON(req *http.Request) { req.Header.Set("Content-Type", "application/json") } @@ -168,7 +203,7 @@ {"host", config.DBHost()}, {"port", config.DBPort()}, {"database", config.DBName()}, - {"schema", databaseScheme}, + {"schema", models.DatabaseScheme}, {"user", config.DBUser()}, {"passwd", config.DBPassword()}, {"dbtype", databaseType}, @@ -214,6 +249,9 @@ auth = basicAuth(user, password) ) + datastoreURL := geoURL + "/rest/workspaces/" + workspaceName + + "/datastores/" + datastoreName + tables := models.InternalServices.Filter(models.IntWFS) if len(tables) == 0 { log.Println("info: no tables to publish") @@ -242,9 +280,7 @@ // Fetch all featuretypes. req, err := http.NewRequest( http.MethodGet, - geoURL+"/rest/workspaces/"+workspaceName+ - "/datastores/"+datastoreName+ - "/featuretypes.json", + datastoreURL+"/featuretypes.json", nil) if err != nil { return err @@ -286,26 +322,50 @@ // Create featuretype. log.Printf("info: creating featuretype %s.\n", table) - // Create featuretype - ft := map[string]interface{}{ - "featureType": map[string]interface{}{ - "name": table, - "nativeName": table, - "title": table, - }, - } + var req *http.Request + if models.IntSQLView(tables[i]) { + // XXX: Creating SQL views with JSON via GeoServer REST-API fails + // Begin code for handling with XML instead + ft := ftXML{ + Name: table, + Title: table, + SRS: tables[i].SRS, + Metadata: ftMetadata{ + Entry: ftMetadataEntry{ + Key: "JDBC_VIRTUAL_TABLE", + VirtTable: ftVirtTable{ + Name: table, + SQL: *tables[i].SQL}}}} - req, err := http.NewRequest( - http.MethodPost, - geoURL+"/rest/workspaces/"+workspaceName+ - "/datastores/"+datastoreName+ - "/featuretypes", - toStream(ft)) - if err != nil { - return err + req, err = http.NewRequest( + http.MethodPost, + datastoreURL+"/featuretypes", + toXMLStream(ft)) + if err != nil { + return err + } + asContentType(req, "text/xml") + // End code for handling with XML instead + } else { + ft := map[string]interface{}{ + "featureType": map[string]interface{}{ + "name": table, + "nativeName": table, + "title": table, + }, + } + + req, err = http.NewRequest( + http.MethodPost, + datastoreURL+"/featuretypes", + toStream(ft)) + if err != nil { + return err + } + asJSON(req) } auth(req) - asJSON(req) + resp, err := http.DefaultClient.Do(req) if err != nil { return err diff -r 7caf620dda50 -r d2eac69ba86b pkg/models/intservices.go --- a/pkg/models/intservices.go Fri Oct 04 11:58:37 2019 +0200 +++ b/pkg/models/intservices.go Fri Oct 04 16:40:04 2019 +0200 @@ -25,11 +25,16 @@ "gemma.intevation.de/gemma/pkg/config" ) +const DatabaseScheme = "waterway" + type IntEntry struct { - Name string `json:"name"` - Style bool `json:"style"` - WMS bool `json:"wms"` - WFS bool `json:"wfs"` + Schema string `json:"schema"` + Name string `json:"name"` + SQL *string `json:"sql"` + SRS *string `json:"srs"` + Style bool `json:"style"` + WMS bool `json:"wms"` + WFS bool `json:"wfs"` } type IntServices struct { @@ -39,20 +44,23 @@ const ( selectServicesSQL = ` -SELECT relname, style IS NOT NULL, as_wms, as_wfs +SELECT schema, name, + view_def, auth_name || ':' || auth_srid, + style IS NOT NULL, as_wms, as_wfs FROM sys_admin.published_services -JOIN pg_class ON name = oid ORDER by relname` + LEFT JOIN spatial_ref_sys USING (srid) +WHERE schema = $1 +ORDER by name` selectStyleSQL = ` SELECT XMLSERIALIZE(DOCUMENT style AS text) FROM sys_admin.published_services -JOIN pg_class ON name = oid -WHERE relname = $1` +WHERE name = $1 AND schema = $2` updateStyleSQL = ` UPDATE sys_admin.published_services SET style = XMLPARSE(DOCUMENT $1) -WHERE name IN (SELECT oid FROM pg_class WHERE relname = $2)` +WHERE name = $2 AND schema = $3` ) var InternalServices = &IntServices{} @@ -65,7 +73,7 @@ return conn.QueryRowContext( ctx, selectStyleSQL, - e.Name).Scan(&style) + e.Name, e.Schema).Scan(&style) }) return style, err } @@ -74,7 +82,7 @@ return auth.RunAsSessionUser(req, func(conn *sql.Conn) error { _, err := conn.ExecContext( req.Context(), updateStyleSQL, - style, name) + style, name, DatabaseScheme) if err == nil { InternalServices.Invalidate() } @@ -124,7 +132,7 @@ return auth.RunAs(ctx, "sys_admin", func(conn *sql.Conn) error { rows, err := conn.QueryContext( - ctx, selectServicesSQL) + ctx, selectServicesSQL, DatabaseScheme) if err != nil { return err } @@ -132,7 +140,8 @@ for rows.Next() { var entry IntEntry if err := rows.Scan( - &entry.Name, &entry.Style, + &entry.Schema, &entry.Name, + &entry.SQL, &entry.SRS, &entry.Style, &entry.WMS, &entry.WFS, ); err != nil { return err @@ -152,6 +161,7 @@ func InternalAll(IntEntry) bool { return true } func IntWMS(entry IntEntry) bool { return entry.WMS } func IntWFS(entry IntEntry) bool { return entry.WFS } +func IntSQLView(entry IntEntry) bool { return entry.SQL != nil } func IntWithStyle(entry IntEntry) bool { return entry.Style } func IntByName(name string) func(IntEntry) bool { diff -r 7caf620dda50 -r d2eac69ba86b schema/default_sysconfig.sql --- a/schema/default_sysconfig.sql Fri Oct 04 11:58:37 2019 +0200 +++ b/schema/default_sysconfig.sql Fri Oct 04 16:40:04 2019 +0200 @@ -23,20 +23,253 @@ -- -- Tables and views published via GeoServer -- -INSERT INTO sys_admin.published_services (name) VALUES - ('waterway.sections_geoserver'), - ('waterway.stretches_geoserver'), - ('waterway.fairway_dimensions'), - ('waterway.gauges_geoserver'), - ('waterway.distance_marks_ashore_geoserver'), - ('waterway.distance_marks_geoserver'), - ('waterway.sounding_results_contour_lines_geoserver'), - ('waterway.bottlenecks_geoserver'), - ('waterway.bottleneck_overview'), - ('waterway.waterway_axis'), - ('waterway.waterway_area'), - ('waterway.waterway_profiles'), - ('waterway.sounding_differences'); + +-- temporary table to store portions of SQL shared by multiple SQL views +CREATE TEMP TABLE base_views (name, def) AS VALUES ( + 'gauges_base_view', $$ + SELECT + g.location, + isrs_asText(g.location) AS isrs_code, + g.objname, + g.geom, + g.applicability_from_km, + g.applicability_to_km, + g.validity, + g.zero_point, + g.geodref, + g.date_info, + g.source_organization, + g.erased, + r.rwls AS reference_water_levels, + wl.measure_date AS gm_measuredate, + wl.water_level AS gm_waterlevel, + wl.n AS gm_n_14d, + fca.forecast_accuracy_3d, + fca.forecast_accuracy_1d + FROM waterway.gauges g + LEFT JOIN (SELECT location, validity, + json_strip_nulls(json_object_agg( + coalesce(depth_reference, 'empty'), value)) AS rwls + FROM waterway.gauges_reference_water_levels + GROUP BY location, validity) AS r + USING (location, validity) + LEFT JOIN (SELECT DISTINCT ON (location) + location, + date_issue, + measure_date, + water_level, + count(*) OVER (PARTITION BY location) AS n + FROM waterway.gauge_measurements + -- consider all measurements within 14 days plus a tolerance + WHERE measure_date + >= current_timestamp - '14 days 00:15'::interval + ORDER BY location, measure_date DESC) AS wl + USING (location) + LEFT JOIN (SELECT DISTINCT ON (location) + location, + date_issue, + max(acc) FILTER (WHERE measure_date + <= current_timestamp + '1 day'::interval) + OVER loc_date_issue AS forecast_accuracy_1d, + max(acc) OVER loc_date_issue AS forecast_accuracy_3d + FROM (SELECT location, date_issue, measure_date, + GREATEST(water_level - lower(conf_interval), + upper(conf_interval) - water_level) AS acc + FROM waterway.gauge_predictions + -- consider predictions made within last 14 days ... + WHERE date_issue + >= current_timestamp - '14 days 00:15'::interval + -- ... for the next three days from now + AND measure_date BETWEEN current_timestamp + AND current_timestamp + '3 days'::interval) AS acc + WINDOW loc_date_issue AS (PARTITION BY location, date_issue) + ORDER BY location, date_issue DESC) AS fca + -- Show only forecasts issued with latest measurements or later + ON fca.location = g.location AND fca.date_issue >= wl.date_issue + $$); + +INSERT INTO sys_admin.published_services (schema, name, srid, view_def) VALUES + -- Directly accessed tables + ('waterway', 'waterway_axis', NULL, NULL), + ('waterway', 'waterway_area', NULL, NULL), + ('waterway', 'waterway_profiles', NULL, NULL), + ('waterway', 'fairway_dimensions', NULL, NULL), + -- GeoServer SQL views + ('waterway', 'gauges_geoserver', 4326, $$ + SELECT + isrs_code, + objname, + geom, + applicability_from_km, + applicability_to_km, + zero_point, + geodref, + date_info, + source_organization, + reference_water_levels, + gm_measuredate, + gm_waterlevel, + gm_n_14d, + forecast_accuracy_3d, + forecast_accuracy_1d + FROM ( + $$ || (SELECT def FROM base_views WHERE name = 'gauges_base_view') || $$ + ) AS gauges_base_view + WHERE NOT erased + $$), + ('waterway', 'sections_geoserver', 4326, $$ + SELECT + s.id, + s.name, + (s.section).lower::varchar as lower, + (s.section).upper::varchar as upper, + s.area, + s.objnam, + s.nobjnam, + s.date_info, + s.source_organization, + s.staging_done, + min(g.gm_measuredate) AS gm_measuredate, + min(g.gm_n_14d) AS gm_n_14d, + max(g.forecast_accuracy_3d) AS forecast_accuracy_3d, + max(g.forecast_accuracy_1d) AS forecast_accuracy_1d + FROM waterway.sections s + LEFT JOIN ( + $$ || (SELECT def FROM base_views WHERE name = 'gauges_base_view') || $$ + WHERE NOT erased) AS g + ON g.location <@ s.section + GROUP BY s.id + $$), + ('waterway', 'stretches_geoserver', 4326, $$ + SELECT + s.id, + s.name, + (s.stretch).lower::varchar as lower, + (s.stretch).upper::varchar as upper, + s.area, + s.objnam, + s.nobjnam, + s.date_info, + s.source_organization, + (SELECT string_agg(country, ', ') + FROM users.stretch_countries + WHERE stretch_id = s.id) AS countries, + s.staging_done, + min(g.gm_measuredate) AS gm_measuredate, + min(g.gm_n_14d) AS gm_n_14d, + max(g.forecast_accuracy_3d) AS forecast_accuracy_3d, + max(g.forecast_accuracy_1d) AS forecast_accuracy_1d + FROM users.stretches s + LEFT JOIN ( + $$ || (SELECT def FROM base_views WHERE name = 'gauges_base_view') || $$ + WHERE NOT erased) AS g + ON g.location <@ s.stretch + GROUP BY s.id + $$), + ('waterway', 'distance_marks_ashore_geoserver', 4326, $$ + SELECT id, + country, + geom, + related_enc, + hectom, + catdis, + position_code + FROM waterway.distance_marks + $$), + ('waterway', 'distance_marks_geoserver', 4326, $$ + SELECT + isrs_asText(location_code) AS location, + geom, + related_enc, + (location_code).hectometre + FROM waterway.distance_marks_virtual + $$), + ('waterway', 'sounding_results_contour_lines_geoserver', 4326, $$ + SELECT bottleneck_id, + date_info, + height, + lines + FROM waterway.sounding_results_contour_lines cl + JOIN waterway.sounding_results sr ON sr.id = cl.sounding_result_id + $$), + ('waterway', 'bottlenecks_geoserver', 4326, $$ + SELECT + b.id, + b.bottleneck_id, + b.objnam, + b.nobjnm, + b.area, + b.rb, + b.lb, + b.responsible_country, + b.revisiting_time, + b.limiting, + b.date_info, + b.source_organization, + g.objname AS gauge_objname, + g.reference_water_levels, + fal.date_info AS fa_date_info, + fal.critical AS fa_critical, + g.gm_measuredate, + g.gm_waterlevel, + g.gm_n_14d, + srl.date_max, + g.forecast_accuracy_3d, + g.forecast_accuracy_1d + FROM waterway.bottlenecks b + LEFT JOIN ( + $$ || (SELECT def FROM base_views WHERE name = 'gauges_base_view') || $$ + ) AS g + ON b.gauge_location = g.location + AND g.validity @> current_timestamp + LEFT JOIN (SELECT DISTINCT ON (bottleneck_id) + bottleneck_id, date_info, critical + FROM waterway.fairway_availability + ORDER BY bottleneck_id, date_info DESC) AS fal + ON b.bottleneck_id = fal.bottleneck_id + LEFT JOIN (SELECT DISTINCT ON (bottleneck_id) + bottleneck_id, max(date_info) AS date_max + FROM waterway.sounding_results + GROUP BY bottleneck_id + ORDER BY bottleneck_id DESC) AS srl + ON b.bottleneck_id = srl.bottleneck_id + WHERE b.validity @> current_timestamp + $$), + ('waterway', 'bottleneck_overview', 4326, $$ + SELECT + objnam AS name, + ST_Centroid(area) AS point, + (lower(stretch)).hectometre AS from, + (upper(stretch)).hectometre AS to, + sr.current::text, + responsible_country + FROM waterway.bottlenecks bn LEFT JOIN ( + SELECT bottleneck_id, max(date_info) AS current + FROM waterway.sounding_results + GROUP BY bottleneck_id) sr + ON sr.bottleneck_id = bn.bottleneck_id + WHERE bn.validity @> current_timestamp + ORDER BY objnam + $$), + ('waterway', 'sounding_differences', 4326, $$ + SELECT + sd.id AS id, + bn.objnam AS objnam, + srm.date_info AS minuend, + srs.date_info AS subtrahend, + sdcl.height AS height, + sdcl.lines AS lines + FROM caching.sounding_differences sd + JOIN caching.sounding_differences_contour_lines sdcl + ON sd.id = sdcl.sounding_differences_id + JOIN waterway.sounding_results srm + ON sd.minuend = srm.id + JOIN waterway.sounding_results srs + ON sd.subtrahend = srs.id + JOIN waterway.bottlenecks bn + ON srm.bottleneck_id = bn.bottleneck_id + AND srm.date_info::timestamptz <@ bn.validity + $$); -- -- Settings diff -r 7caf620dda50 -r d2eac69ba86b schema/gemma.sql --- a/schema/gemma.sql Fri Oct 04 11:58:37 2019 +0200 +++ b/schema/gemma.sql Fri Oct 04 16:40:04 2019 +0200 @@ -23,6 +23,26 @@ CREATE EXTENSION btree_gist; -- +-- Functions to be used in CHECK constraints +-- + +-- Check if a given string can be used as a FROM item in an SQL statement +CREATE OR REPLACE FUNCTION is_valid_from_item(stmt text) RETURNS boolean +AS $$ +BEGIN + EXECUTE format('SELECT * FROM (%s) AS test', stmt); + RETURN true; +EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE USING MESSAGE = SQLERRM, ERRCODE = SQLSTATE; + RETURN false; +END +$$ + LANGUAGE plpgsql + STRICT; + + +-- -- Trigger functions -- -- TODO: will there ever be UPDATEs or can we drop that function due to @@ -361,10 +381,20 @@ ) CREATE TABLE published_services ( - name regclass PRIMARY KEY, + schema varchar CHECK(to_regnamespace(schema) IS NOT NULL), + name varchar, + PRIMARY KEY (schema, name), + -- SQL statement used for an SQL view in GeoServer: + view_def text CHECK (is_valid_from_item(view_def)), + -- SRID to be used with SQL view: + srid int REFERENCES spatial_ref_sys, + -- SLD style document: style xml CHECK(style IS DOCUMENT), as_wms boolean NOT NULL DEFAULT TRUE, - as_wfs boolean NOT NULL DEFAULT TRUE + as_wfs boolean NOT NULL DEFAULT TRUE, + -- Either give a valid relation or a SQL statement: + CHECK (to_regclass(schema || '.' || name) IS NOT NULL + OR view_def IS NOT NULL) ) ; diff -r 7caf620dda50 -r d2eac69ba86b schema/gemma_tests.sql --- a/schema/gemma_tests.sql Fri Oct 04 11:58:37 2019 +0200 +++ b/schema/gemma_tests.sql Fri Oct 04 16:40:04 2019 +0200 @@ -15,6 +15,15 @@ -- pgTAP test script for gemma schema definition -- +SELECT ok(is_valid_from_item('SELECT * FROM sys_admin.published_services'), + 'Valid statement passes check'); + +SELECT ok(NOT is_valid_from_item('This is not SQL'), + 'Arbitrary text does not pass check'); + +SELECT ok(is_valid_from_item(NULL) IS NULL, + 'NULL value is not checked'); + SELECT throws_ok($$ INSERT INTO waterway.waterway_axis (wtwaxs, objnam) VALUES (ST_GeogFromText('LINESTRING(0 0, 1 1)'), 'test'), diff -r 7caf620dda50 -r d2eac69ba86b schema/geoserver_views.sql --- a/schema/geoserver_views.sql Fri Oct 04 11:58:37 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,225 +0,0 @@ -CREATE OR REPLACE VIEW waterway.gauges_base_view AS - SELECT - g.location, - isrs_asText(g.location) AS isrs_code, - g.objname, - g.geom, - g.applicability_from_km, - g.applicability_to_km, - g.validity, - g.zero_point, - g.geodref, - g.date_info, - g.source_organization, - g.erased, - r.rwls AS reference_water_levels, - wl.measure_date AS gm_measuredate, - wl.water_level AS gm_waterlevel, - wl.n AS gm_n_14d, - fca.forecast_accuracy_3d, - fca.forecast_accuracy_1d - FROM waterway.gauges g - LEFT JOIN (SELECT location, validity, - json_strip_nulls(json_object_agg( - coalesce(depth_reference, 'empty'), value)) AS rwls - FROM waterway.gauges_reference_water_levels - GROUP BY location, validity) AS r - USING (location, validity) - LEFT JOIN (SELECT DISTINCT ON (location) - location, - date_issue, - measure_date, - water_level, - count(*) OVER (PARTITION BY location) AS n - FROM waterway.gauge_measurements - -- consider all measurements within 14 days plus a tolerance - WHERE measure_date - >= current_timestamp - '14 days 00:15'::interval - ORDER BY location, measure_date DESC) AS wl - USING (location) - LEFT JOIN (SELECT DISTINCT ON (location) - location, - date_issue, - max(acc) FILTER (WHERE measure_date - <= current_timestamp + '1 day'::interval) - OVER loc_date_issue AS forecast_accuracy_1d, - max(acc) OVER loc_date_issue AS forecast_accuracy_3d - FROM (SELECT location, date_issue, measure_date, - GREATEST(water_level - lower(conf_interval), - upper(conf_interval) - water_level) AS acc - FROM waterway.gauge_predictions - -- consider predictions made within last 14 days ... - WHERE date_issue - >= current_timestamp - '14 days 00:15'::interval - -- ... for the next three days from now - AND measure_date BETWEEN current_timestamp - AND current_timestamp + '3 days'::interval) AS acc - WINDOW loc_date_issue AS (PARTITION BY location, date_issue) - ORDER BY location, date_issue DESC) AS fca - -- Show only forecasts issued with latest measurements or later - ON fca.location = g.location AND fca.date_issue >= wl.date_issue; - -CREATE OR REPLACE VIEW waterway.gauges_geoserver AS - SELECT - isrs_code, - objname, - geom, - applicability_from_km, - applicability_to_km, - zero_point, - geodref, - date_info, - source_organization, - reference_water_levels, - gm_measuredate, - gm_waterlevel, - gm_n_14d, - forecast_accuracy_3d, - forecast_accuracy_1d - FROM waterway.gauges_base_view - WHERE NOT erased; - -CREATE OR REPLACE VIEW waterway.distance_marks_geoserver AS - SELECT - isrs_asText(location_code) AS location, - geom::Geometry(POINT, 4326), - related_enc, - (location_code).hectometre - FROM waterway.distance_marks_virtual; - -CREATE OR REPLACE VIEW waterway.distance_marks_ashore_geoserver AS - SELECT id, - country, - geom::Geometry(POINT, 4326), - related_enc, - hectom, - catdis, - position_code - FROM waterway.distance_marks; - -CREATE OR REPLACE VIEW waterway.bottlenecks_geoserver AS - SELECT - b.id, - b.bottleneck_id, - b.objnam, - b.nobjnm, - b.area, - b.rb, - b.lb, - b.responsible_country, - b.revisiting_time, - b.limiting, - b.date_info, - b.source_organization, - g.objname AS gauge_objname, - g.reference_water_levels, - fal.date_info AS fa_date_info, - fal.critical AS fa_critical, - g.gm_measuredate, - g.gm_waterlevel, - g.gm_n_14d, - srl.date_max, - g.forecast_accuracy_3d, - g.forecast_accuracy_1d - FROM waterway.bottlenecks b - LEFT JOIN waterway.gauges_base_view g - ON b.gauge_location = g.location AND g.validity @> current_timestamp - LEFT JOIN (SELECT DISTINCT ON (bottleneck_id) - bottleneck_id, date_info, critical - FROM waterway.fairway_availability - ORDER BY bottleneck_id, date_info DESC) AS fal - ON b.bottleneck_id = fal.bottleneck_id - LEFT JOIN (SELECT DISTINCT ON (bottleneck_id) - bottleneck_id, max(date_info) AS date_max - FROM waterway.sounding_results - GROUP BY bottleneck_id - ORDER BY bottleneck_id DESC) AS srl - ON b.bottleneck_id = srl.bottleneck_id - WHERE b.validity @> current_timestamp; - -CREATE OR REPLACE VIEW waterway.stretches_geoserver AS - SELECT - s.id, - s.name, - (s.stretch).lower::varchar as lower, - (s.stretch).upper::varchar as upper, - s.area::Geometry(MULTIPOLYGON, 4326), - s.objnam, - s.nobjnam, - s.date_info, - s.source_organization, - (SELECT string_agg(country, ', ') - FROM users.stretch_countries - WHERE stretch_id = s.id) AS countries, - s.staging_done, - min(g.gm_measuredate) AS gm_measuredate, - min(g.gm_n_14d) AS gm_n_14d, - max(g.forecast_accuracy_3d) AS forecast_accuracy_3d, - max(g.forecast_accuracy_1d) AS forecast_accuracy_1d - FROM users.stretches s - LEFT JOIN waterway.gauges_geoserver g - ON isrs_fromtext(g.isrs_code) <@ s.stretch - GROUP BY s.id; - -CREATE OR REPLACE VIEW waterway.sections_geoserver AS - SELECT - s.id, - s.name, - (s.section).lower::varchar as lower, - (s.section).upper::varchar as upper, - s.area::Geometry(MULTIPOLYGON, 4326), - s.objnam, - s.nobjnam, - s.date_info, - s.source_organization, - s.staging_done, - min(g.gm_measuredate) AS gm_measuredate, - min(g.gm_n_14d) AS gm_n_14d, - max(g.forecast_accuracy_3d) AS forecast_accuracy_3d, - max(g.forecast_accuracy_1d) AS forecast_accuracy_1d - FROM waterway.sections s - LEFT JOIN waterway.gauges_geoserver g - ON isrs_fromtext(g.isrs_code) <@ s.section - GROUP BY s.id; - -CREATE OR REPLACE VIEW waterway.sounding_results_contour_lines_geoserver AS - SELECT bottleneck_id, - date_info, - height, - CAST(lines AS geometry(multilinestring, 4326)) AS lines - FROM waterway.sounding_results_contour_lines cl - JOIN waterway.sounding_results sr ON sr.id = cl.sounding_result_id; - -CREATE OR REPLACE VIEW waterway.bottleneck_overview AS - SELECT - objnam AS name, - ST_Centroid(area)::Geometry(POINT, 4326) AS point, - (lower(stretch)).hectometre AS from, - (upper(stretch)).hectometre AS to, - sr.current::text, - responsible_country - FROM waterway.bottlenecks bn LEFT JOIN ( - SELECT bottleneck_id, max(date_info) AS current - FROM waterway.sounding_results - GROUP BY bottleneck_id) sr ON sr.bottleneck_id = bn.bottleneck_id - WHERE bn.validity @> current_timestamp - ORDER BY objnam; - -CREATE OR REPLACE VIEW waterway.sounding_differences AS - SELECT - sd.id AS id, - bn.objnam AS objnam, - srm.date_info AS minuend, - srs.date_info AS subtrahend, - sdcl.height AS height, - CAST(sdcl.lines AS geometry(multilinestring, 4326)) AS lines - FROM caching.sounding_differences sd - JOIN caching.sounding_differences_contour_lines sdcl - ON sd.id = sdcl.sounding_differences_id - JOIN waterway.sounding_results srm - ON sd.minuend = srm.id - JOIN waterway.sounding_results srs - ON sd.subtrahend = srs.id - JOIN waterway.bottlenecks bn - ON srm.bottleneck_id = bn.bottleneck_id - AND srm.date_info::timestamptz <@ bn.validity; diff -r 7caf620dda50 -r d2eac69ba86b schema/install-db.sh --- a/schema/install-db.sh Fri Oct 04 11:58:37 2019 +0200 +++ b/schema/install-db.sh Fri Oct 04 16:40:04 2019 +0200 @@ -130,7 +130,6 @@ -f "$BASEDIR/search_functions.sql" \ -f "$BASEDIR/geonames.sql" \ -f "$BASEDIR/manage_users.sql" \ - -f "$BASEDIR/geoserver_views.sql" \ -f "$BASEDIR/auth.sql" \ -f "$BASEDIR/isrs_functions.sql" \ -f "$BASEDIR/default_sysconfig.sql" \ diff -r 7caf620dda50 -r d2eac69ba86b schema/run_tests.sh --- a/schema/run_tests.sh Fri Oct 04 11:58:37 2019 +0200 +++ b/schema/run_tests.sh Fri Oct 04 16:40:04 2019 +0200 @@ -80,7 +80,7 @@ -c 'SET client_min_messages TO WARNING' \ -c "DROP ROLE IF EXISTS $TEST_ROLES" \ -f "$BASEDIR"/tap_tests_data.sql \ - -c "SELECT plan(75 + ( + -c "SELECT plan(78 + ( SELECT count(*)::int FROM information_schema.tables WHERE table_schema = 'waterway'))" \ diff -r 7caf620dda50 -r d2eac69ba86b schema/updates/1300/01.add_view_def.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/updates/1300/01.add_view_def.sql Fri Oct 04 16:40:04 2019 +0200 @@ -0,0 +1,28 @@ +CREATE OR REPLACE FUNCTION is_valid_from_item(stmt text) RETURNS boolean +AS $$ +BEGIN + EXECUTE format('SELECT * FROM (%s) AS test', stmt); + RETURN true; +EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE USING MESSAGE = SQLERRM, ERRCODE = SQLSTATE; + RETURN false; +END +$$ + LANGUAGE plpgsql + STRICT; + +ALTER TABLE sys_admin.published_services + ADD schema varchar; +UPDATE sys_admin.published_services + SET schema = substring(CAST(name AS varchar) from '(.*)\.'); +ALTER TABLE sys_admin.published_services + ADD CHECK(to_regnamespace(schema) IS NOT NULL), + ALTER name TYPE varchar + USING substring(CAST(name AS varchar) from '\.(.*)'), + DROP CONSTRAINT published_services_pkey, + ADD PRIMARY KEY (schema, name), + ADD view_def text CHECK (is_valid_from_item(view_def)), + ADD srid int REFERENCES spatial_ref_sys, + ADD CHECK (to_regclass(schema || '.' || name) IS NOT NULL + OR view_def IS NOT NULL); diff -r 7caf620dda50 -r d2eac69ba86b schema/updates/1300/02.views_to_geoservers.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/updates/1300/02.views_to_geoservers.sql Fri Oct 04 16:40:04 2019 +0200 @@ -0,0 +1,257 @@ +-- Copied from ../../default_sysconfig.sql +-- plus conflict resolution to achieve updates + +CREATE TEMP TABLE base_views (name, def) AS VALUES ( + 'gauges_base_view', $$ + SELECT + g.location, + isrs_asText(g.location) AS isrs_code, + g.objname, + g.geom, + g.applicability_from_km, + g.applicability_to_km, + g.validity, + g.zero_point, + g.geodref, + g.date_info, + g.source_organization, + g.erased, + r.rwls AS reference_water_levels, + wl.measure_date AS gm_measuredate, + wl.water_level AS gm_waterlevel, + wl.n AS gm_n_14d, + fca.forecast_accuracy_3d, + fca.forecast_accuracy_1d + FROM waterway.gauges g + LEFT JOIN (SELECT location, validity, + json_strip_nulls(json_object_agg( + coalesce(depth_reference, 'empty'), value)) AS rwls + FROM waterway.gauges_reference_water_levels + GROUP BY location, validity) AS r + USING (location, validity) + LEFT JOIN (SELECT DISTINCT ON (location) + location, + date_issue, + measure_date, + water_level, + count(*) OVER (PARTITION BY location) AS n + FROM waterway.gauge_measurements + -- consider all measurements within 14 days plus a tolerance + WHERE measure_date + >= current_timestamp - '14 days 00:15'::interval + ORDER BY location, measure_date DESC) AS wl + USING (location) + LEFT JOIN (SELECT DISTINCT ON (location) + location, + date_issue, + max(acc) FILTER (WHERE measure_date + <= current_timestamp + '1 day'::interval) + OVER loc_date_issue AS forecast_accuracy_1d, + max(acc) OVER loc_date_issue AS forecast_accuracy_3d + FROM (SELECT location, date_issue, measure_date, + GREATEST(water_level - lower(conf_interval), + upper(conf_interval) - water_level) AS acc + FROM waterway.gauge_predictions + -- consider predictions made within last 14 days ... + WHERE date_issue + >= current_timestamp - '14 days 00:15'::interval + -- ... for the next three days from now + AND measure_date BETWEEN current_timestamp + AND current_timestamp + '3 days'::interval) AS acc + WINDOW loc_date_issue AS (PARTITION BY location, date_issue) + ORDER BY location, date_issue DESC) AS fca + -- Show only forecasts issued with latest measurements or later + ON fca.location = g.location AND fca.date_issue >= wl.date_issue + $$); + +INSERT INTO sys_admin.published_services (schema, name, srid, view_def) VALUES + ('waterway', 'gauges_geoserver', 4326, $$ + SELECT + isrs_code, + objname, + geom, + applicability_from_km, + applicability_to_km, + zero_point, + geodref, + date_info, + source_organization, + reference_water_levels, + gm_measuredate, + gm_waterlevel, + gm_n_14d, + forecast_accuracy_3d, + forecast_accuracy_1d + FROM ( + $$ || (SELECT def FROM base_views WHERE name = 'gauges_base_view') || $$ + ) AS gauges_base_view + WHERE NOT erased + $$), + ('waterway', 'sections_geoserver', 4326, $$ + SELECT + s.id, + s.name, + (s.section).lower::varchar as lower, + (s.section).upper::varchar as upper, + s.area::Geometry(MULTIPOLYGON, 4326), + s.objnam, + s.nobjnam, + s.date_info, + s.source_organization, + s.staging_done, + min(g.gm_measuredate) AS gm_measuredate, + min(g.gm_n_14d) AS gm_n_14d, + max(g.forecast_accuracy_3d) AS forecast_accuracy_3d, + max(g.forecast_accuracy_1d) AS forecast_accuracy_1d + FROM waterway.sections s + LEFT JOIN ( + $$ || (SELECT def FROM base_views WHERE name = 'gauges_base_view') || $$ + WHERE NOT erased) AS g + ON g.location <@ s.section + GROUP BY s.id + $$), + ('waterway', 'stretches_geoserver', 4326, $$ + SELECT + s.id, + s.name, + (s.stretch).lower::varchar as lower, + (s.stretch).upper::varchar as upper, + s.area::Geometry(MULTIPOLYGON, 4326), + s.objnam, + s.nobjnam, + s.date_info, + s.source_organization, + (SELECT string_agg(country, ', ') + FROM users.stretch_countries + WHERE stretch_id = s.id) AS countries, + s.staging_done, + min(g.gm_measuredate) AS gm_measuredate, + min(g.gm_n_14d) AS gm_n_14d, + max(g.forecast_accuracy_3d) AS forecast_accuracy_3d, + max(g.forecast_accuracy_1d) AS forecast_accuracy_1d + FROM users.stretches s + LEFT JOIN ( + $$ || (SELECT def FROM base_views WHERE name = 'gauges_base_view') || $$ + WHERE NOT erased) AS g + ON g.location <@ s.stretch + GROUP BY s.id + $$), + ('waterway', 'distance_marks_ashore_geoserver', 4326, $$ + SELECT id, + country, + geom::Geometry(POINT, 4326), + related_enc, + hectom, + catdis, + position_code + FROM waterway.distance_marks + $$), + ('waterway', 'distance_marks_geoserver', 4326, $$ + SELECT + isrs_asText(location_code) AS location, + geom::Geometry(POINT, 4326), + related_enc, + (location_code).hectometre + FROM waterway.distance_marks_virtual + $$), + ('waterway', 'sounding_results_contour_lines_geoserver', 4326, $$ + SELECT bottleneck_id, + date_info, + height, + CAST(lines AS geometry(multilinestring, 4326)) AS lines + FROM waterway.sounding_results_contour_lines cl + JOIN waterway.sounding_results sr ON sr.id = cl.sounding_result_id + $$), + ('waterway', 'bottlenecks_geoserver', 4326, $$ + SELECT + b.id, + b.bottleneck_id, + b.objnam, + b.nobjnm, + b.area, + b.rb, + b.lb, + b.responsible_country, + b.revisiting_time, + b.limiting, + b.date_info, + b.source_organization, + g.objname AS gauge_objname, + g.reference_water_levels, + fal.date_info AS fa_date_info, + fal.critical AS fa_critical, + g.gm_measuredate, + g.gm_waterlevel, + g.gm_n_14d, + srl.date_max, + g.forecast_accuracy_3d, + g.forecast_accuracy_1d + FROM waterway.bottlenecks b + LEFT JOIN ( + $$ || (SELECT def FROM base_views WHERE name = 'gauges_base_view') || $$ + ) AS g + ON b.gauge_location = g.location + AND g.validity @> current_timestamp + LEFT JOIN (SELECT DISTINCT ON (bottleneck_id) + bottleneck_id, date_info, critical + FROM waterway.fairway_availability + ORDER BY bottleneck_id, date_info DESC) AS fal + ON b.bottleneck_id = fal.bottleneck_id + LEFT JOIN (SELECT DISTINCT ON (bottleneck_id) + bottleneck_id, max(date_info) AS date_max + FROM waterway.sounding_results + GROUP BY bottleneck_id + ORDER BY bottleneck_id DESC) AS srl + ON b.bottleneck_id = srl.bottleneck_id + WHERE b.validity @> current_timestamp + $$), + ('waterway', 'bottleneck_overview', 4326, $$ + SELECT + objnam AS name, + ST_Centroid(area)::Geometry(POINT, 4326) AS point, + (lower(stretch)).hectometre AS from, + (upper(stretch)).hectometre AS to, + sr.current::text, + responsible_country + FROM waterway.bottlenecks bn LEFT JOIN ( + SELECT bottleneck_id, max(date_info) AS current + FROM waterway.sounding_results + GROUP BY bottleneck_id) sr + ON sr.bottleneck_id = bn.bottleneck_id + WHERE bn.validity @> current_timestamp + ORDER BY objnam + $$), + ('waterway', 'sounding_differences', 4326, $$ + SELECT + sd.id AS id, + bn.objnam AS objnam, + srm.date_info AS minuend, + srs.date_info AS subtrahend, + sdcl.height AS height, + CAST(sdcl.lines AS geometry(multilinestring, 4326)) AS lines + FROM caching.sounding_differences sd + JOIN caching.sounding_differences_contour_lines sdcl + ON sd.id = sdcl.sounding_differences_id + JOIN waterway.sounding_results srm + ON sd.minuend = srm.id + JOIN waterway.sounding_results srs + ON sd.subtrahend = srs.id + JOIN waterway.bottlenecks bn + ON srm.bottleneck_id = bn.bottleneck_id + AND srm.date_info::timestamptz <@ bn.validity + $$) +ON CONFLICT (schema, name) DO UPDATE SET + srid = EXCLUDED.srid, + view_def = EXCLUDED.view_def; + +DROP VIEW + waterway.gauges_base_view, + waterway.gauges_geoserver, + waterway.distance_marks_geoserver, + waterway.distance_marks_ashore_geoserver, + waterway.bottlenecks_geoserver, + waterway.stretches_geoserver, + waterway.sections_geoserver, + waterway.sounding_results_contour_lines_geoserver, + waterway.bottleneck_overview, + waterway.sounding_differences; diff -r 7caf620dda50 -r d2eac69ba86b schema/version.sql --- a/schema/version.sql Fri Oct 04 11:58:37 2019 +0200 +++ b/schema/version.sql Fri Oct 04 16:40:04 2019 +0200 @@ -1,1 +1,1 @@ -INSERT INTO gemma_schema_version(version) VALUES (1203); +INSERT INTO gemma_schema_version(version) VALUES (1300);