Build spec and host brief carried in from C:\Claude and WRPS/02-env; the plant model (equipment, tags, alarm bitmask, enums, unit conversions) is derived from WRPS/04-plc/register-map.csv, WRPS/05-scada/modbus/scada-points.csv and WRPS-CTL-003. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
139 lines
6.6 KiB
SQL
139 lines
6.6 KiB
SQL
-- =============================================================================
|
|
-- 001_schema.sql — pg-ai reference data and document store.
|
|
--
|
|
-- psql -h pg-ai -U postgres -d plant -f 001_schema.sql
|
|
--
|
|
-- What pg-ai is FOR: pgvector document chunks, Cube pre-aggregations, and the
|
|
-- equipment/tag reference data including the alias lists.
|
|
--
|
|
-- What pg-ai is NOT for: historian data. There is no replication job and no
|
|
-- mirror table. imh is already an isolated copy of the raw SCADA historian, so
|
|
-- Cube queries imh directly over TDS/1433 with a read-only login. The only
|
|
-- exception is 002_fixtures.sql, which stands in for imh until it exists.
|
|
--
|
|
-- Storage is UTC everywhere. Conversion to SITE_TIMEZONE happens exactly once,
|
|
-- in Cube. Never in SQL here and never in a prompt.
|
|
-- =============================================================================
|
|
|
|
CREATE EXTENSION IF NOT EXISTS vector;
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- equipment — what the operator says.
|
|
--
|
|
-- "Pump 02" is equipment; its data lives on tags. Without this table every
|
|
-- equipment-level question fails, because no historian point is called
|
|
-- "Pump 02". Aliases are the whole point: operators do not type tag numbers.
|
|
-- -----------------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS equipment (
|
|
equipment_id TEXT PRIMARY KEY, -- PU-302
|
|
display_name TEXT, -- Pump 02
|
|
aliases TEXT[], -- {'Pump 02','pump2','P2','PU-302'}
|
|
equipment_type TEXT, -- pump | vessel | station | piping | ...
|
|
unit_name TEXT, -- WRPS
|
|
description TEXT
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS equipment_aliases_gin ON equipment USING gin (aliases);
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- tags — the historian points and field instruments, and how to read them.
|
|
--
|
|
-- engineering_unit, range and setpoints are recorded in the units the HISTORIAN
|
|
-- stores, which are not always the units the PLC works in. Wet well level is
|
|
-- the trap: the PLC works in mm, CI Server historises percent of the spill weir
|
|
-- crest (mm / 60). The description field carries the conversion for every tag
|
|
-- where the two differ. Read it before interpreting a number.
|
|
--
|
|
-- The description also records whether a tag IS HISTORISED. Field inputs to the
|
|
-- PLC (%IW / %IX) are not published to SCADA and have no history at all. An
|
|
-- answer that trends PU-301 vibration is fabricating data.
|
|
-- -----------------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS tags (
|
|
tag_id TEXT PRIMARY KEY, -- PS_STN_WET_WELL_LEVEL, LIT-101
|
|
equipment_id TEXT REFERENCES equipment(equipment_id),
|
|
display_name TEXT,
|
|
aliases TEXT[],
|
|
signal_type TEXT, -- level|flow|pressure|status|state|...
|
|
engineering_unit TEXT,
|
|
range_low DOUBLE PRECISION,
|
|
range_high DOUBLE PRECISION,
|
|
alarm_setpoint_hi DOUBLE PRECISION,
|
|
alarm_setpoint_lo DOUBLE PRECISION,
|
|
trip_setpoint DOUBLE PRECISION,
|
|
description TEXT
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS tags_aliases_gin ON tags USING gin (aliases);
|
|
CREATE INDEX IF NOT EXISTS tags_equipment_ix ON tags (equipment_id);
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- doc_chunks — controlled documents, chunked and embedded.
|
|
--
|
|
-- superseded and effective_date matter more than they look. Citing a withdrawn
|
|
-- revision of a procedure is worse than finding nothing at all, so retrieval
|
|
-- filters superseded = FALSE by default and the citation always carries the
|
|
-- revision and effective date.
|
|
-- -----------------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS doc_chunks (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
source_file TEXT NOT NULL,
|
|
doc_type TEXT NOT NULL, -- procedure|manual|rationalisation|design
|
|
doc_number TEXT, -- WRPS-CTL-002
|
|
revision TEXT,
|
|
effective_date DATE,
|
|
superseded BOOLEAN DEFAULT FALSE,
|
|
equipment_id TEXT,
|
|
page INT,
|
|
section_title TEXT,
|
|
chunk_text TEXT NOT NULL,
|
|
embedding VECTOR(1536), -- text-embedding-3-small
|
|
created_at TIMESTAMPTZ DEFAULT now(),
|
|
CONSTRAINT doc_chunks_type_ck
|
|
CHECK (doc_type IN ('procedure','manual','rationalisation','design'))
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS doc_chunks_embedding_hnsw
|
|
ON doc_chunks USING hnsw (embedding vector_cosine_ops);
|
|
CREATE INDEX IF NOT EXISTS doc_chunks_live_type
|
|
ON doc_chunks (doc_type) WHERE superseded = FALSE;
|
|
CREATE INDEX IF NOT EXISTS doc_chunks_source
|
|
ON doc_chunks (source_file);
|
|
CREATE INDEX IF NOT EXISTS doc_chunks_equipment
|
|
ON doc_chunks (equipment_id);
|
|
|
|
-- Re-ingesting a file replaces its chunks; it must never duplicate them.
|
|
-- ingest.py deletes by source_file inside the same transaction as the insert.
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- Cube writes its pre-aggregations into their own schema, with its own role.
|
|
-- -----------------------------------------------------------------------------
|
|
CREATE SCHEMA IF NOT EXISTS cube_preagg;
|
|
|
|
-- =============================================================================
|
|
-- Seed load. Aliases are pipe-separated in the CSVs because a Postgres array
|
|
-- literal inside CSV is unreadable and unmergeable in review. Load via a
|
|
-- staging table and split on load.
|
|
--
|
|
-- psql -h pg-ai -U postgres -d plant -v ON_ERROR_STOP=1 <<'PSQL'
|
|
-- \i 001_schema.sql
|
|
-- CREATE TEMP TABLE eq_stage (LIKE equipment INCLUDING ALL);
|
|
-- ALTER TABLE eq_stage ALTER COLUMN aliases TYPE TEXT;
|
|
-- \copy eq_stage FROM 'seed/equipment.csv' WITH (FORMAT csv, HEADER true)
|
|
-- INSERT INTO equipment
|
|
-- SELECT equipment_id, display_name, string_to_array(aliases, '|'),
|
|
-- equipment_type, unit_name, description
|
|
-- FROM eq_stage
|
|
-- ON CONFLICT (equipment_id) DO UPDATE SET
|
|
-- display_name = EXCLUDED.display_name, aliases = EXCLUDED.aliases,
|
|
-- equipment_type = EXCLUDED.equipment_type, unit_name = EXCLUDED.unit_name,
|
|
-- description = EXCLUDED.description;
|
|
-- PSQL
|
|
--
|
|
-- Same shape for tags. scripts/deploy.sh does this for you.
|
|
--
|
|
-- Phase 1 gate: every equipment item and every tag has at least one
|
|
-- human-friendly alias. Check it, do not assume it:
|
|
--
|
|
-- SELECT equipment_id FROM equipment WHERE coalesce(array_length(aliases,1),0) = 0;
|
|
-- SELECT tag_id FROM tags WHERE coalesce(array_length(aliases,1),0) = 0;
|
|
-- =============================================================================
|