1 contributor
473 lines | 18.788kb
-- autoSMART Database Schema v2.0 — Normalizare SMART parameters din JSONB
-- Version: 2.0
-- Description: Replaces blob-based JSONB storage with structured EAV model
-- Date: 2026-05-20
-- This schema REPLACES smart_readings with smart_collection_events + smart_param_values
-- Backward compat: v_smart_readings_compat view emulates old schema

-- ============================================================================
-- EXTENSIONS
-- ============================================================================

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "btree_gin";

-- ============================================================================
-- TIMESTAMP TRIGGER (reusable)
-- ============================================================================

CREATE OR REPLACE FUNCTION update_timestamp() RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- ============================================================================
-- SMART PARAMETER CATALOG — Registru parametri cu auto-discovery
-- ============================================================================

CREATE TABLE smart_param_catalog (
    id                  SERIAL PRIMARY KEY,
    param_name          VARCHAR(100) NOT NULL,
    param_id_ata        INTEGER,
    device_type         VARCHAR(20) DEFAULT 'any',
    unit                VARCHAR(20),
    description         TEXT,
    warning_threshold   NUMERIC,
    critical_threshold  NUMERIC,
    health_weight       NUMERIC DEFAULT 1.0,
    is_critical         BOOLEAN DEFAULT false,
    lower_is_better     BOOLEAN DEFAULT true,
    track_raw_value     BOOLEAN DEFAULT true,
    track_normalized    BOOLEAN DEFAULT false,
    first_seen          TIMESTAMPTZ DEFAULT NOW(),
    created_at          TIMESTAMPTZ DEFAULT NOW(),
    updated_at          TIMESTAMPTZ DEFAULT NOW(),

    CONSTRAINT uq_param_name UNIQUE (param_name)
);

CREATE INDEX idx_param_catalog_name ON smart_param_catalog(param_name);
CREATE INDEX idx_param_catalog_critical ON smart_param_catalog(is_critical) WHERE is_critical = true;

CREATE TRIGGER update_param_catalog_timestamp
    BEFORE UPDATE ON smart_param_catalog
    FOR EACH ROW EXECUTE FUNCTION update_timestamp();

-- ============================================================================
-- SMART COLLECTION EVENTS — Metadate per-colectare (înlocuiește smart_readings)
-- ============================================================================

CREATE TABLE smart_collection_events (
    id              BIGSERIAL PRIMARY KEY,
    hdd_id          INTEGER NOT NULL REFERENCES hdd_inventory(id),
    serial_number   VARCHAR(100) NOT NULL,
    node_id         VARCHAR(50) NOT NULL,
    collected_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    collection_ok   BOOLEAN DEFAULT true,
    temperature     SMALLINT,
    checksum        VARCHAR(64),
    param_count     SMALLINT,
    notes           TEXT
);

CREATE INDEX idx_sce_hdd_id_time  ON smart_collection_events(hdd_id, collected_at DESC);
CREATE INDEX idx_sce_serial       ON smart_collection_events(serial_number);
CREATE INDEX idx_sce_node_time    ON smart_collection_events(node_id, collected_at DESC);
CREATE INDEX idx_sce_collected_at ON smart_collection_events(collected_at DESC);
CREATE INDEX idx_sce_checksum     ON smart_collection_events(checksum) WHERE checksum IS NOT NULL;

-- ============================================================================
-- SMART PARAM VALUES — Tabel EAV partiționat lunar
-- ============================================================================

CREATE TABLE smart_param_values (
    id              BIGSERIAL,
    event_id        BIGINT NOT NULL,
    hdd_id          INTEGER NOT NULL,
    param_id        INTEGER NOT NULL REFERENCES smart_param_catalog(id),
    collected_at    TIMESTAMPTZ NOT NULL,
    raw_value       BIGINT,
    normalized_value SMALLINT,
    worst_value     SMALLINT,
    threshold_value SMALLINT,
    when_failed     VARCHAR(20),

    PRIMARY KEY (collected_at, id)
) PARTITION BY RANGE (collected_at);

-- Creare partiții pentru perioada viitoare (2025-2027)
CREATE TABLE smart_param_values_2025_01 PARTITION OF smart_param_values
    FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
CREATE TABLE smart_param_values_2025_02 PARTITION OF smart_param_values
    FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
CREATE TABLE smart_param_values_2025_03 PARTITION OF smart_param_values
    FOR VALUES FROM ('2025-03-01') TO ('2025-04-01');
CREATE TABLE smart_param_values_2025_04 PARTITION OF smart_param_values
    FOR VALUES FROM ('2025-04-01') TO ('2025-05-01');
CREATE TABLE smart_param_values_2025_05 PARTITION OF smart_param_values
    FOR VALUES FROM ('2025-05-01') TO ('2025-06-01');
CREATE TABLE smart_param_values_2025_06 PARTITION OF smart_param_values
    FOR VALUES FROM ('2025-06-01') TO ('2025-07-01');
CREATE TABLE smart_param_values_2025_07 PARTITION OF smart_param_values
    FOR VALUES FROM ('2025-07-01') TO ('2025-08-01');
CREATE TABLE smart_param_values_2025_08 PARTITION OF smart_param_values
    FOR VALUES FROM ('2025-08-01') TO ('2025-09-01');
CREATE TABLE smart_param_values_2025_09 PARTITION OF smart_param_values
    FOR VALUES FROM ('2025-09-01') TO ('2025-10-01');
CREATE TABLE smart_param_values_2025_10 PARTITION OF smart_param_values
    FOR VALUES FROM ('2025-10-01') TO ('2025-11-01');
CREATE TABLE smart_param_values_2025_11 PARTITION OF smart_param_values
    FOR VALUES FROM ('2025-11-01') TO ('2025-12-01');
CREATE TABLE smart_param_values_2025_12 PARTITION OF smart_param_values
    FOR VALUES FROM ('2025-12-01') TO ('2026-01-01');
CREATE TABLE smart_param_values_2026_01 PARTITION OF smart_param_values
    FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE smart_param_values_2026_02 PARTITION OF smart_param_values
    FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE smart_param_values_2026_03 PARTITION OF smart_param_values
    FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
CREATE TABLE smart_param_values_2026_04 PARTITION OF smart_param_values
    FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
CREATE TABLE smart_param_values_2026_05 PARTITION OF smart_param_values
    FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
CREATE TABLE smart_param_values_2026_06 PARTITION OF smart_param_values
    FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
CREATE TABLE smart_param_values_2026_07 PARTITION OF smart_param_values
    FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
CREATE TABLE smart_param_values_2026_08 PARTITION OF smart_param_values
    FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');
CREATE TABLE smart_param_values_2026_09 PARTITION OF smart_param_values
    FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
CREATE TABLE smart_param_values_2026_10 PARTITION OF smart_param_values
    FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
CREATE TABLE smart_param_values_2026_11 PARTITION OF smart_param_values
    FOR VALUES FROM ('2026-11-01') TO ('2026-12-01');
CREATE TABLE smart_param_values_2026_12 PARTITION OF smart_param_values
    FOR VALUES FROM ('2026-12-01') TO ('2027-01-01');

-- Partiție default pentru date neprevăzute
CREATE TABLE smart_param_values_default PARTITION OF smart_param_values DEFAULT;

-- Indexuri pe partiții
CREATE INDEX idx_spv_hdd_param_time ON smart_param_values(hdd_id, param_id, collected_at DESC);
CREATE INDEX idx_spv_event_id ON smart_param_values(event_id);
CREATE INDEX idx_spv_param_time ON smart_param_values(param_id, collected_at DESC);
CREATE INDEX idx_spv_critical_values ON smart_param_values(hdd_id, collected_at DESC)
    WHERE raw_value > 0;

-- ============================================================================
-- VIEWS
-- ============================================================================

-- View 1: Ultimele valori per disc per parametru
CREATE VIEW v_latest_param_values AS
SELECT DISTINCT ON (spv.hdd_id, spv.param_id)
    spv.hdd_id,
    spv.param_id,
    spc.param_name,
    spc.unit,
    spc.is_critical,
    spv.raw_value,
    spv.normalized_value,
    spv.collected_at,
    hi.serial_number,
    hi.model_name,
    hi.current_node_id
FROM smart_param_values spv
JOIN smart_param_catalog spc ON spv.param_id = spc.id
JOIN hdd_inventory hi ON spv.hdd_id = hi.id
WHERE hi.status = 'active'
ORDER BY spv.hdd_id, spv.param_id, spv.collected_at DESC;

-- View 2: Starea curentă a fiecărui disc
CREATE VIEW v_drive_health_summary AS
SELECT
    hi.id              AS hdd_id,
    hi.serial_number,
    hi.model_name,
    hi.manufacturer,
    hi.current_node_id,
    hi.current_device_path,
    hi.status,
    hi.size_gb,
    sce_last.collected_at   AS last_collection,
    sce_last.temperature    AS last_temperature,
    sce_last.collection_ok  AS last_collection_ok,
    EXTRACT(EPOCH FROM (NOW() - sce_last.collected_at)) / 3600 AS hours_since_collection,
    critical_counts.params_above_zero AS critical_params_triggered,
    pred.risk_level,
    pred.failure_probability,
    pred.predicted_failure_date
FROM hdd_inventory hi
LEFT JOIN LATERAL (
    SELECT collected_at, temperature, collection_ok
    FROM smart_collection_events
    WHERE hdd_id = hi.id
    ORDER BY collected_at DESC
    LIMIT 1
) sce_last ON true
LEFT JOIN LATERAL (
    SELECT COUNT(*) AS params_above_zero
    FROM smart_param_values spv
    JOIN smart_param_catalog spc ON spv.param_id = spc.id
    WHERE spv.hdd_id = hi.id
      AND spc.is_critical = true
      AND spv.raw_value > 0
      AND spv.collected_at >= COALESCE((
          SELECT collected_at FROM smart_collection_events
          WHERE hdd_id = hi.id ORDER BY collected_at DESC LIMIT 1
      ), NOW()) - INTERVAL '5 minutes'
) critical_counts ON true
LEFT JOIN LATERAL (
    SELECT risk_level, failure_probability, predicted_failure_date
    FROM predictions
    WHERE hdd_id = hi.id
    ORDER BY timestamp DESC
    LIMIT 1
) pred ON true
WHERE hi.status = 'active';

-- View 3: Trending parametru specific
CREATE VIEW v_param_trend AS
SELECT
    hi.serial_number,
    hi.model_name,
    hi.current_node_id,
    spc.param_name,
    spc.unit,
    spc.is_critical,
    spc.warning_threshold,
    spc.critical_threshold,
    spv.raw_value,
    spv.normalized_value,
    spv.collected_at,
    sce.node_id AS collected_on_node,
    sce.temperature AS drive_temp_at_collection
FROM smart_param_values spv
JOIN smart_collection_events sce ON spv.event_id = sce.id
JOIN smart_param_catalog spc ON spv.param_id = spc.id
JOIN hdd_inventory hi ON spv.hdd_id = hi.id;

-- View 4: Cluster overview
CREATE VIEW v_cluster_overview AS
SELECT
    hi.current_node_id AS node,
    COUNT(*) AS total_drives,
    COUNT(*) FILTER (WHERE dhs.last_collection > NOW() - INTERVAL '1 hour') AS drives_recently_collected,
    COUNT(*) FILTER (WHERE dhs.critical_params_triggered > 0) AS drives_with_critical_params,
    COUNT(*) FILTER (WHERE dhs.risk_level IN ('high', 'critical')) AS drives_high_risk,
    MAX(dhs.last_collection) AS latest_collection,
    MIN(dhs.last_collection) AS oldest_collection
FROM hdd_inventory hi
JOIN v_drive_health_summary dhs ON hi.id = dhs.hdd_id
WHERE hi.status = 'active'
GROUP BY hi.current_node_id;

-- View 5: Compatibilitate cu codul existent (emulează smart_readings)
CREATE VIEW v_smart_readings_compat AS
SELECT
    sce.id,
    sce.hdd_id,
    sce.serial_number,
    sce.collected_at AS timestamp,
    sce.temperature,
    sce.node_id,
    sce.collection_ok,
    jsonb_object_agg(
        COALESCE(spc.param_name, ''),
        spv.raw_value
    ) FILTER (WHERE spc.param_name IS NOT NULL) AS parameters_json
FROM smart_collection_events sce
LEFT JOIN smart_param_values spv ON spv.event_id = sce.id
LEFT JOIN smart_param_catalog spc ON spv.param_id = spc.id
GROUP BY sce.id, sce.hdd_id, sce.serial_number, sce.collected_at, sce.temperature, sce.node_id, sce.collection_ok;

-- ============================================================================
-- FUNCTIONS
-- ============================================================================

-- Upsert parametru în catalog (auto-discovery)
CREATE OR REPLACE FUNCTION upsert_param_catalog(
    p_param_name    VARCHAR(100),
    p_param_id_ata  INTEGER DEFAULT NULL,
    p_unit          VARCHAR(20) DEFAULT 'count'
) RETURNS INTEGER AS $$
DECLARE
    v_id INTEGER;
BEGIN
    INSERT INTO smart_param_catalog (param_name, param_id_ata, unit, first_seen)
    VALUES (p_param_name, p_param_id_ata, p_unit, NOW())
    ON CONFLICT (param_name) DO UPDATE
        SET param_id_ata = COALESCE(EXCLUDED.param_id_ata, smart_param_catalog.param_id_ata),
            updated_at = NOW()
    RETURNING id INTO v_id;
    RETURN v_id;
END;
$$ LANGUAGE plpgsql;

-- Inserare atomică event + valori (înlocuiește logica de collection)
CREATE OR REPLACE FUNCTION insert_collection_event(
    p_hdd_id        INTEGER,
    p_serial        VARCHAR(100),
    p_node_id       VARCHAR(50),
    p_collected_at  TIMESTAMPTZ,
    p_temperature   SMALLINT,
    p_collection_ok BOOLEAN,
    p_checksum      VARCHAR(64),
    p_params        JSONB
) RETURNS BIGINT AS $$
DECLARE
    v_event_id  BIGINT;
    v_param_id  INTEGER;
    v_param     RECORD;
    v_count     SMALLINT := 0;
BEGIN
    INSERT INTO smart_collection_events
        (hdd_id, serial_number, node_id, collected_at,
         collection_ok, temperature, checksum, param_count)
    VALUES
        (p_hdd_id, p_serial, p_node_id, p_collected_at,
         p_collection_ok, p_temperature, p_checksum, 0)
    RETURNING id INTO v_event_id;

    FOR v_param IN SELECT * FROM jsonb_each(p_params)
    LOOP
        v_param_id := upsert_param_catalog(
            v_param.key,
            (v_param.value->>'id')::INTEGER,
            'count'
        );

        INSERT INTO smart_param_values
            (event_id, hdd_id, param_id, collected_at,
             raw_value, normalized_value, worst_value, threshold_value, when_failed)
        VALUES
            (v_event_id, p_hdd_id, v_param_id, p_collected_at,
             (v_param.value->>'raw_value')::BIGINT,
             (v_param.value->>'value')::SMALLINT,
             (v_param.value->>'worst')::SMALLINT,
             (v_param.value->>'thresh')::SMALLINT,
             v_param.value->>'when_failed');

        v_count := v_count + 1;
    END LOOP;

    UPDATE smart_collection_events SET param_count = v_count WHERE id = v_event_id;

    RETURN v_event_id;
END;
$$ LANGUAGE plpgsql;

-- Creare partiție lunară (cu idempotență)
CREATE OR REPLACE FUNCTION create_monthly_partition(p_year INTEGER, p_month INTEGER)
RETURNS VOID AS $$
DECLARE
    v_table_name    TEXT;
    v_start_date    DATE;
    v_end_date      DATE;
BEGIN
    v_table_name := format('smart_param_values_%s_%s',
        p_year, lpad(p_month::TEXT, 2, '0'));
    v_start_date := make_date(p_year, p_month, 1);
    v_end_date   := v_start_date + INTERVAL '1 month';

    IF NOT EXISTS (
        SELECT 1 FROM information_schema.tables
        WHERE table_name = v_table_name AND table_schema = 'public'
    ) THEN
        EXECUTE format(
            'CREATE TABLE %I PARTITION OF smart_param_values
             FOR VALUES FROM (%L) TO (%L)',
            v_table_name, v_start_date, v_end_date
        );
        RAISE NOTICE 'Created partition: %', v_table_name;
    END IF;
END;
$$ LANGUAGE plpgsql;

-- On-demand data deletion: delete all data for a specific HDD (by serial_number)
-- RETENTION POLICY: On-demand only (no automatic cleanup)
-- Called from frontend when user requests to remove a drive from monitoring
CREATE OR REPLACE FUNCTION delete_hdd_data_by_serial(
    p_serial_number VARCHAR(100),
    p_keep_catalog  BOOLEAN DEFAULT true
) RETURNS TABLE(
    deleted_values BIGINT,
    deleted_events BIGINT,
    deleted_catalog INTEGER
) AS $$
DECLARE
    v_hdd_id            INTEGER;
    v_deleted_values    BIGINT := 0;
    v_deleted_events    BIGINT := 0;
    v_deleted_catalog   INTEGER := 0;
BEGIN
    -- Get HDD ID for this serial
    SELECT id INTO v_hdd_id FROM hdd_inventory WHERE serial_number = p_serial_number;

    IF v_hdd_id IS NULL THEN
        RAISE NOTICE 'Serial % not found in inventory', p_serial_number;
        RETURN QUERY SELECT 0::BIGINT, 0::BIGINT, 0::INTEGER;
        RETURN;
    END IF;

    -- Delete from smart_param_values (cascade via event_id FK)
    DELETE FROM smart_param_values spv
    WHERE event_id IN (
        SELECT id FROM smart_collection_events
        WHERE hdd_id = v_hdd_id
    );
    GET DIAGNOSTICS v_deleted_values = ROW_COUNT;

    -- Delete from smart_collection_events
    DELETE FROM smart_collection_events
    WHERE hdd_id = v_hdd_id OR serial_number = p_serial_number;
    GET DIAGNOSTICS v_deleted_events = ROW_COUNT;

    -- Optionally delete from hdd_inventory and catalog
    IF NOT p_keep_catalog THEN
        DELETE FROM smart_param_catalog spc
        WHERE NOT EXISTS (
            SELECT 1 FROM smart_param_values
            WHERE param_id = spc.id
        );
        GET DIAGNOSTICS v_deleted_catalog = ROW_COUNT;

        DELETE FROM hdd_inventory WHERE id = v_hdd_id;
    END IF;

    RAISE NOTICE 'Deleted % param values, % events for serial %',
        v_deleted_values, v_deleted_events, p_serial_number;

    RETURN QUERY SELECT v_deleted_values, v_deleted_events, v_deleted_catalog;
END;
$$ LANGUAGE plpgsql;

-- ============================================================================
-- PERMISSIONS
-- ============================================================================

GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO autosmart;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO autosmart;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO autosmart;

-- Specific grants for collections
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE smart_param_catalog TO autosmart;
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE smart_collection_events TO autosmart;
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE smart_param_values TO autosmart;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO autosmart;

-- ============================================================================
-- FINAL MESSAGE
-- ============================================================================

DO $$
BEGIN
    RAISE NOTICE '✅ autoSMART Schema v2.0 deployed successfully!';
    RAISE NOTICE 'New tables: smart_param_catalog, smart_collection_events, smart_param_values (partitioned)';
    RAISE NOTICE 'Views: v_latest_param_values, v_drive_health_summary, v_param_trend, v_cluster_overview, v_smart_readings_compat';
    RAISE NOTICE 'Functions: upsert_param_catalog, insert_collection_event, create_monthly_partition, delete_hdd_data_by_serial';
    RAISE NOTICE 'Next step: Run sql/migrate-v1-to-v2.sql to migrate existing data';
END $$;