feat(workbench): add runtime GIS and sample integrations
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
-- Append-only task alert events plus a monotonic current-state projection.
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS workbench;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workbench.alert_events (
|
||||
project_id uuid NOT NULL,
|
||||
producer_id text NOT NULL CHECK (length(producer_id) > 0),
|
||||
event_id uuid NOT NULL,
|
||||
alert_id text NOT NULL CHECK (length(alert_id) > 0),
|
||||
event_type text NOT NULL CHECK (event_type IN ('raised','acknowledged','resolved')),
|
||||
occurred_at timestamptz NOT NULL,
|
||||
ingested_at timestamptz NOT NULL DEFAULT now(),
|
||||
document jsonb NOT NULL,
|
||||
PRIMARY KEY (project_id,producer_id,event_id),
|
||||
CHECK ((jsonb_typeof(document)='object'
|
||||
AND document->>'producer_id'=producer_id
|
||||
AND document->>'event_id'=event_id::text
|
||||
AND document->>'alert_id'=alert_id
|
||||
AND document->>'event_type'=event_type
|
||||
AND (document->>'occurred_at')::timestamptz=occurred_at) IS TRUE)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS alert_events_alert_idx
|
||||
ON workbench.alert_events(project_id,alert_id,occurred_at,event_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workbench.alerts (
|
||||
project_id uuid NOT NULL,
|
||||
alert_id text NOT NULL CHECK (length(alert_id) > 0),
|
||||
revision bigint NOT NULL CHECK (revision >= 1),
|
||||
status text NOT NULL CHECK (status IN ('active','acknowledged','resolved')),
|
||||
occurred_at timestamptz NOT NULL,
|
||||
updated_at timestamptz NOT NULL,
|
||||
document jsonb NOT NULL,
|
||||
PRIMARY KEY (project_id,alert_id),
|
||||
CHECK ((jsonb_typeof(document)='object'
|
||||
AND document->>'alert_id'=alert_id
|
||||
AND document->>'status'=status
|
||||
AND (document->>'occurred_at')::timestamptz=occurred_at) IS TRUE)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS alerts_active_recent_idx
|
||||
ON workbench.alerts(project_id,occurred_at DESC,alert_id)
|
||||
WHERE status='active';
|
||||
|
||||
REVOKE ALL ON ALL TABLES IN SCHEMA workbench FROM PUBLIC;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,12 @@
|
||||
-- Bound and index the persisted analysis reads used by the workbench and API.
|
||||
-- Safe to re-run after analysis.runs and analysis.results are provisioned.
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS runs_created_at_run_id_idx
|
||||
ON analysis.runs(created_at DESC, run_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS results_run_created_at_result_id_idx
|
||||
ON analysis.results(run_id, created_at DESC, result_id DESC);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,74 @@
|
||||
CREATE SCHEMA IF NOT EXISTS source_gis;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS source_gis.datasets (
|
||||
dataset_key TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
source_date DATE NOT NULL,
|
||||
declared_srid INTEGER,
|
||||
effective_srid INTEGER NOT NULL,
|
||||
coordinate_note TEXT NOT NULL,
|
||||
imported_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS source_gis.layers (
|
||||
layer_key TEXT PRIMARY KEY,
|
||||
dataset_key TEXT NOT NULL REFERENCES source_gis.datasets(dataset_key) ON DELETE CASCADE,
|
||||
display_name TEXT NOT NULL,
|
||||
source_file TEXT NOT NULL,
|
||||
feature_kind TEXT NOT NULL,
|
||||
geometry_type TEXT NOT NULL,
|
||||
feature_count BIGINT NOT NULL CHECK (feature_count >= 0),
|
||||
imported_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS source_gis.features (
|
||||
layer_key TEXT NOT NULL REFERENCES source_gis.layers(layer_key) ON DELETE CASCADE,
|
||||
source_fid BIGINT NOT NULL,
|
||||
source_object_id TEXT,
|
||||
source_keyword TEXT,
|
||||
source_asset_id TEXT,
|
||||
source_diameter DOUBLE PRECISION,
|
||||
from_node_id TEXT,
|
||||
to_node_id TEXT,
|
||||
attributes JSONB NOT NULL CHECK (jsonb_typeof(attributes) = 'object'),
|
||||
geom geometry(Geometry, 900914) NOT NULL,
|
||||
PRIMARY KEY (layer_key, source_fid)
|
||||
);
|
||||
|
||||
ALTER TABLE source_gis.features
|
||||
ADD COLUMN IF NOT EXISTS source_diameter DOUBLE PRECISION;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS source_gis.element_links (
|
||||
element_type TEXT NOT NULL,
|
||||
element_id TEXT NOT NULL,
|
||||
layer_key TEXT NOT NULL,
|
||||
source_fid BIGINT NOT NULL,
|
||||
match_method TEXT NOT NULL,
|
||||
distance_m DOUBLE PRECISION,
|
||||
PRIMARY KEY (element_type, element_id, layer_key),
|
||||
FOREIGN KEY (layer_key, source_fid)
|
||||
REFERENCES source_gis.features(layer_key, source_fid)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_source_gis_layers_dataset
|
||||
ON source_gis.layers(dataset_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_source_gis_features_object_id
|
||||
ON source_gis.features(layer_key, source_object_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_source_gis_features_nodes
|
||||
ON source_gis.features(layer_key, from_node_id, to_node_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_source_gis_features_nodes_reverse
|
||||
ON source_gis.features(layer_key, to_node_id, from_node_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_source_gis_features_geom
|
||||
ON source_gis.features USING GIST (geom);
|
||||
CREATE INDEX IF NOT EXISTS idx_source_gis_element_links_lookup
|
||||
ON source_gis.element_links(element_type, element_id);
|
||||
|
||||
COMMENT ON SCHEMA source_gis IS
|
||||
'Imported source GIS inventories retained separately from the hydraulic model.';
|
||||
COMMENT ON COLUMN source_gis.datasets.declared_srid IS
|
||||
'SRID declared by the source file, which may be incorrect.';
|
||||
COMMENT ON COLUMN source_gis.datasets.effective_srid IS
|
||||
'SRID assigned after validating source coordinates against the project model.';
|
||||
COMMENT ON COLUMN source_gis.features.attributes IS
|
||||
'Original non-geometry GIS fields, including null values, preserved as JSONB.';
|
||||
@@ -0,0 +1,28 @@
|
||||
-- Read-only work-order projection synchronized by an external work-order source.
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS workbench;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workbench.work_orders (
|
||||
project_id uuid NOT NULL,
|
||||
work_order_id text NOT NULL CHECK (length(work_order_id) > 0),
|
||||
scheduled_at timestamptz NOT NULL,
|
||||
updated_at timestamptz NOT NULL,
|
||||
document jsonb NOT NULL,
|
||||
PRIMARY KEY (project_id, work_order_id),
|
||||
CHECK ((jsonb_typeof(document) = 'object'
|
||||
AND document->>'id' = work_order_id
|
||||
AND (document->>'scheduled_at')::timestamptz = scheduled_at
|
||||
AND (document->>'updated_at')::timestamptz = updated_at) IS TRUE)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS work_orders_project_schedule_idx
|
||||
ON workbench.work_orders(project_id, scheduled_at, work_order_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS work_orders_project_updated_idx
|
||||
ON workbench.work_orders(project_id, updated_at DESC, work_order_id);
|
||||
|
||||
REVOKE ALL ON workbench.work_orders FROM PUBLIC;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,13 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE peripheral_sample.demo_datasets
|
||||
DROP CONSTRAINT IF EXISTS demo_datasets_document_check;
|
||||
|
||||
ALTER TABLE peripheral_sample.demo_datasets
|
||||
ADD CONSTRAINT demo_datasets_document_check CHECK ((
|
||||
jsonb_typeof(document) = 'object'
|
||||
AND document->>'origin' = 'synthetic'
|
||||
AND document->>'generator_version' IN ('water-demo@1', 'water-demo@2')
|
||||
) IS TRUE);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,122 @@
|
||||
DO $$
|
||||
DECLARE
|
||||
layer RECORD;
|
||||
BEGIN
|
||||
FOR layer IN
|
||||
SELECT *
|
||||
FROM (VALUES
|
||||
('fx_20200423_drain_valves', 'fx_20200423_drain_valves', 'Point'),
|
||||
('fx_20200423_service_connections', 'fx_20200423_service_connections', 'Point'),
|
||||
('fx_20200423_flow_meters', 'fx_20200423_flow_meters', 'Point'),
|
||||
('fx_20200423_hydrants', 'fx_20200423_hydrants', 'Point'),
|
||||
('fx_20200423_customer_meters', 'fx_20200423_customer_meters', 'Point'),
|
||||
('fx_20200423_pipes', 'fx_20200423_pipes', 'LineString'),
|
||||
('fx_20200423_valves', 'fx_20200423_valves', 'Point'),
|
||||
('fx_20200423_non_residential_meters', 'fx_20200423_non_residential_meters', 'Point'),
|
||||
('fx_20200811_dn100_pipes', 'fx_20200811_dn100_pipes', 'LineString')
|
||||
) AS configured(view_name, layer_key, geometry_type)
|
||||
LOOP
|
||||
IF to_regclass(format('gis.%I', layer.view_name)) IS NULL THEN
|
||||
EXECUTE format(
|
||||
$view$
|
||||
CREATE MATERIALIZED VIEW gis.%I AS
|
||||
SELECT
|
||||
feature.source_fid::text AS id,
|
||||
feature.source_fid,
|
||||
feature.source_object_id,
|
||||
feature.source_diameter AS diameter,
|
||||
CASE
|
||||
WHEN GeometryType(feature.geom) IN ('LINESTRING', 'MULTILINESTRING')
|
||||
THEN ST_Length(feature.geom)
|
||||
ELSE NULL
|
||||
END AS length,
|
||||
dataset.dataset_key,
|
||||
dataset.title AS dataset_title,
|
||||
dataset.source_date,
|
||||
source_layer.layer_key AS source_layer_key,
|
||||
source_layer.display_name AS source_layer_name,
|
||||
source_layer.feature_kind,
|
||||
ST_Transform(feature.geom, 3857)::geometry(%s, 3857) AS geom
|
||||
FROM source_gis.features AS feature
|
||||
JOIN source_gis.layers AS source_layer
|
||||
ON source_layer.layer_key = feature.layer_key
|
||||
JOIN source_gis.datasets AS dataset
|
||||
ON dataset.dataset_key = source_layer.dataset_key
|
||||
WHERE feature.layer_key = %L
|
||||
AND feature.geom IS NOT NULL
|
||||
WITH DATA
|
||||
$view$,
|
||||
layer.view_name,
|
||||
layer.geometry_type,
|
||||
layer.layer_key
|
||||
);
|
||||
END IF;
|
||||
|
||||
EXECUTE format(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS %I ON gis.%I (id)',
|
||||
'ux_' || layer.view_name || '_id',
|
||||
layer.view_name
|
||||
);
|
||||
EXECUTE format(
|
||||
'CREATE INDEX IF NOT EXISTS %I ON gis.%I USING GIST (geom)',
|
||||
'ix_' || layer.view_name || '_geom',
|
||||
layer.view_name
|
||||
);
|
||||
END LOOP;
|
||||
END
|
||||
$$;
|
||||
|
||||
DROP VIEW IF EXISTS gis.water_quality_monitoring_points;
|
||||
|
||||
CREATE VIEW gis.water_quality_monitoring_points AS
|
||||
WITH latest_result AS (
|
||||
SELECT
|
||||
run.run_id,
|
||||
run.name AS source_analysis,
|
||||
run.created_at,
|
||||
result.payload
|
||||
FROM analysis.runs AS run
|
||||
JOIN LATERAL (
|
||||
SELECT stored.payload
|
||||
FROM analysis.results AS stored
|
||||
WHERE stored.run_id = run.run_id
|
||||
AND stored.result_type = 'sensor_placement'
|
||||
ORDER BY stored.created_at DESC, stored.result_id DESC
|
||||
LIMIT 1
|
||||
) AS result ON true
|
||||
WHERE run.run_type = 'sensor_placement'
|
||||
AND run.status = 'completed'
|
||||
AND result.payload->>'sensor_type' = 'water_quality'
|
||||
AND jsonb_typeof(result.payload->'sensor_locations') = 'array'
|
||||
ORDER BY run.created_at DESC, run.run_id DESC
|
||||
LIMIT 1
|
||||
), ranked_locations AS (
|
||||
SELECT
|
||||
latest.run_id,
|
||||
latest.source_analysis,
|
||||
latest.created_at,
|
||||
latest.payload,
|
||||
location.node_id,
|
||||
location.rank
|
||||
FROM latest_result AS latest
|
||||
CROSS JOIN LATERAL jsonb_array_elements_text(
|
||||
latest.payload->'sensor_locations'
|
||||
) WITH ORDINALITY AS location(node_id, rank)
|
||||
)
|
||||
SELECT
|
||||
ranked.node_id AS id,
|
||||
ranked.node_id,
|
||||
ranked.rank::integer AS monitoring_point_no,
|
||||
ranked.run_id,
|
||||
ranked.source_analysis,
|
||||
ranked.created_at,
|
||||
ranked.payload->>'method' AS selection_method,
|
||||
(ranked.payload->>'minimum_diameter')::integer AS minimum_diameter,
|
||||
junction.elevation,
|
||||
junction.geom
|
||||
FROM ranked_locations AS ranked
|
||||
JOIN gis.junctions AS junction
|
||||
ON junction.id = ranked.node_id;
|
||||
|
||||
COMMENT ON VIEW gis.water_quality_monitoring_points IS
|
||||
'Current water-quality monitoring points selected by the sensitivity service.';
|
||||
Reference in New Issue
Block a user