-- ============================================================ -- Rulesets schema (DB: rules) -- ------------------------------------------------------------ -- Un ruleset e' una collezione versionata di "items" (paths -- Signal K per i logs, codici openmeteo per i forecasts, ...). -- Modello: -- * 5 tipi fissi: logs | forecast_current | forecast_hourly -- | marine_current | marine_hourly -- * Un solo ruleset puo' essere "active" per ciascun tipo. -- * Le versioni sono triple di interi 1..100 (major.build.patch). -- * Gli items sono JSONB per massima flessibilita'. -- * Ogni item ha un "ref" stabile scelto dall'utente: e' la -- chiave logica che garantisce continuita' su InfluxDB anche -- se il path del sensore cambia. -- * Le deployments tracciano quale ruleset-version e' stato -- pushato ad ogni sensore. -- ============================================================ CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- ───────────────────────────────────────────────────────────── -- RULESETS -- ───────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS rulesets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), type TEXT NOT NULL CHECK (type IN ('logs','forecast_current','forecast_hourly','marine_current','marine_hourly')), version_major SMALLINT NOT NULL DEFAULT 1 CHECK (version_major BETWEEN 1 AND 100), version_build SMALLINT NOT NULL DEFAULT 0 CHECK (version_build BETWEEN 0 AND 100), version_patch SMALLINT NOT NULL DEFAULT 0 CHECK (version_patch BETWEEN 0 AND 100), description TEXT NOT NULL DEFAULT '', tags TEXT[] NOT NULL DEFAULT '{}', -- items: [{ ref, path, enabled, meta: {...} }, ...] -- ref: identificatore logico stabile (chiave su Influx) -- path: SK path (logs) | codice openmeteo (forecast/marine) -- meta: { name, unit, measurement, sk_path, group_name, category, ... } items JSONB NOT NULL DEFAULT '[]'::jsonb, active BOOLEAN NOT NULL DEFAULT false, archived BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (type, version_major, version_build, version_patch) ); -- solo UN ruleset active per tipo (archiviati esclusi) CREATE UNIQUE INDEX IF NOT EXISTS rulesets_one_active_per_type ON rulesets (type) WHERE active = true AND archived = false; CREATE INDEX IF NOT EXISTS rulesets_type_idx ON rulesets (type); CREATE INDEX IF NOT EXISTS rulesets_active_idx ON rulesets (type) WHERE active = true; CREATE INDEX IF NOT EXISTS rulesets_archived_idx ON rulesets (archived); CREATE INDEX IF NOT EXISTS rulesets_items_gin_idx ON rulesets USING GIN (items); -- Validazione items: array di oggetti con almeno ref+path CREATE OR REPLACE FUNCTION rulesets_validate_items() RETURNS trigger AS $$ DECLARE refs TEXT[]; BEGIN IF jsonb_typeof(NEW.items) <> 'array' THEN RAISE EXCEPTION 'items must be a JSON array'; END IF; -- tutti gli item devono avere ref non vuoto e path (anche vuoto ammesso) IF EXISTS ( SELECT 1 FROM jsonb_array_elements(NEW.items) it WHERE jsonb_typeof(it) <> 'object' OR NULLIF(it->>'ref','') IS NULL ) THEN RAISE EXCEPTION 'every item must be an object with a non-empty "ref"'; END IF; -- unicita' ref all'interno dello stesso ruleset SELECT array_agg(it->>'ref') INTO refs FROM jsonb_array_elements(NEW.items) it; IF (SELECT count(DISTINCT x) FROM unnest(refs) x) <> COALESCE(array_length(refs,1),0) THEN RAISE EXCEPTION 'item refs must be unique within the ruleset'; END IF; NEW.updated_at := NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS rulesets_validate_trigger ON rulesets; CREATE TRIGGER rulesets_validate_trigger BEFORE INSERT OR UPDATE ON rulesets FOR EACH ROW EXECUTE FUNCTION rulesets_validate_items(); -- ───────────────────────────────────────────────────────────── -- DEPLOYMENTS -- Traccia quale ruleset-version e' stato pushato ad ogni -- sensore (per tipo). Un solo ruleset per (sensor,type). -- ───────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS ruleset_deployments ( sensor_name TEXT NOT NULL, type TEXT NOT NULL, ruleset_id UUID NOT NULL REFERENCES rulesets(id) ON DELETE CASCADE, deployed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), acked_at TIMESTAMPTZ, PRIMARY KEY (sensor_name, type) ); CREATE INDEX IF NOT EXISTS ruleset_deployments_ruleset_idx ON ruleset_deployments (ruleset_id); -- ───────────────────────────────────────────────────────────── -- AUDIT LOG (opzionale ma utile) -- ───────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS ruleset_changes ( id BIGSERIAL PRIMARY KEY, ruleset_id UUID, type TEXT, action TEXT NOT NULL, -- created | updated | activated | archived | deleted | deployed user_id TEXT, payload JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS ruleset_changes_ruleset_idx ON ruleset_changes (ruleset_id); CREATE INDEX IF NOT EXISTS ruleset_changes_created_idx ON ruleset_changes (created_at DESC);