Newer Older
473 lines | 18.788kb
Bogdan Timofte authored 2 weeks ago
1
-- autoSMART Database Schema v2.0 — Normalizare SMART parameters din JSONB
2
-- Version: 2.0
3
-- Description: Replaces blob-based JSONB storage with structured EAV model
4
-- Date: 2026-05-20
5
-- This schema REPLACES smart_readings with smart_collection_events + smart_param_values
6
-- Backward compat: v_smart_readings_compat view emulates old schema
7

            
8
-- ============================================================================
9
-- EXTENSIONS
10
-- ============================================================================
11

            
12
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
13
CREATE EXTENSION IF NOT EXISTS "btree_gin";
14

            
15
-- ============================================================================
16
-- TIMESTAMP TRIGGER (reusable)
17
-- ============================================================================
18

            
19
CREATE OR REPLACE FUNCTION update_timestamp() RETURNS TRIGGER AS $$
20
BEGIN
21
    NEW.updated_at = NOW();
22
    RETURN NEW;
23
END;
24
$$ LANGUAGE plpgsql;
25

            
26
-- ============================================================================
27
-- SMART PARAMETER CATALOG — Registru parametri cu auto-discovery
28
-- ============================================================================
29

            
30
CREATE TABLE smart_param_catalog (
31
    id                  SERIAL PRIMARY KEY,
32
    param_name          VARCHAR(100) NOT NULL,
33
    param_id_ata        INTEGER,
34
    device_type         VARCHAR(20) DEFAULT 'any',
35
    unit                VARCHAR(20),
36
    description         TEXT,
37
    warning_threshold   NUMERIC,
38
    critical_threshold  NUMERIC,
39
    health_weight       NUMERIC DEFAULT 1.0,
40
    is_critical         BOOLEAN DEFAULT false,
41
    lower_is_better     BOOLEAN DEFAULT true,
42
    track_raw_value     BOOLEAN DEFAULT true,
43
    track_normalized    BOOLEAN DEFAULT false,
44
    first_seen          TIMESTAMPTZ DEFAULT NOW(),
45
    created_at          TIMESTAMPTZ DEFAULT NOW(),
46
    updated_at          TIMESTAMPTZ DEFAULT NOW(),
47

            
48
    CONSTRAINT uq_param_name UNIQUE (param_name)
49
);
50

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

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

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

            
62
CREATE TABLE smart_collection_events (
63
    id              BIGSERIAL PRIMARY KEY,
64
    hdd_id          INTEGER NOT NULL REFERENCES hdd_inventory(id),
65
    serial_number   VARCHAR(100) NOT NULL,
66
    node_id         VARCHAR(50) NOT NULL,
67
    collected_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
68
    collection_ok   BOOLEAN DEFAULT true,
69
    temperature     SMALLINT,
70
    checksum        VARCHAR(64),
71
    param_count     SMALLINT,
72
    notes           TEXT
73
);
74

            
75
CREATE INDEX idx_sce_hdd_id_time  ON smart_collection_events(hdd_id, collected_at DESC);
76
CREATE INDEX idx_sce_serial       ON smart_collection_events(serial_number);
77
CREATE INDEX idx_sce_node_time    ON smart_collection_events(node_id, collected_at DESC);
78
CREATE INDEX idx_sce_collected_at ON smart_collection_events(collected_at DESC);
79
CREATE INDEX idx_sce_checksum     ON smart_collection_events(checksum) WHERE checksum IS NOT NULL;
80

            
81
-- ============================================================================
82
-- SMART PARAM VALUES — Tabel EAV partiționat lunar
83
-- ============================================================================
84

            
85
CREATE TABLE smart_param_values (
86
    id              BIGSERIAL,
87
    event_id        BIGINT NOT NULL,
88
    hdd_id          INTEGER NOT NULL,
89
    param_id        INTEGER NOT NULL REFERENCES smart_param_catalog(id),
90
    collected_at    TIMESTAMPTZ NOT NULL,
91
    raw_value       BIGINT,
92
    normalized_value SMALLINT,
93
    worst_value     SMALLINT,
94
    threshold_value SMALLINT,
95
    when_failed     VARCHAR(20),
96

            
97
    PRIMARY KEY (collected_at, id)
98
) PARTITION BY RANGE (collected_at);
99

            
100
-- Creare partiții pentru perioada viitoare (2025-2027)
101
CREATE TABLE smart_param_values_2025_01 PARTITION OF smart_param_values
102
    FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
103
CREATE TABLE smart_param_values_2025_02 PARTITION OF smart_param_values
104
    FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
105
CREATE TABLE smart_param_values_2025_03 PARTITION OF smart_param_values
106
    FOR VALUES FROM ('2025-03-01') TO ('2025-04-01');
107
CREATE TABLE smart_param_values_2025_04 PARTITION OF smart_param_values
108
    FOR VALUES FROM ('2025-04-01') TO ('2025-05-01');
109
CREATE TABLE smart_param_values_2025_05 PARTITION OF smart_param_values
110
    FOR VALUES FROM ('2025-05-01') TO ('2025-06-01');
111
CREATE TABLE smart_param_values_2025_06 PARTITION OF smart_param_values
112
    FOR VALUES FROM ('2025-06-01') TO ('2025-07-01');
113
CREATE TABLE smart_param_values_2025_07 PARTITION OF smart_param_values
114
    FOR VALUES FROM ('2025-07-01') TO ('2025-08-01');
115
CREATE TABLE smart_param_values_2025_08 PARTITION OF smart_param_values
116
    FOR VALUES FROM ('2025-08-01') TO ('2025-09-01');
117
CREATE TABLE smart_param_values_2025_09 PARTITION OF smart_param_values
118
    FOR VALUES FROM ('2025-09-01') TO ('2025-10-01');
119
CREATE TABLE smart_param_values_2025_10 PARTITION OF smart_param_values
120
    FOR VALUES FROM ('2025-10-01') TO ('2025-11-01');
121
CREATE TABLE smart_param_values_2025_11 PARTITION OF smart_param_values
122
    FOR VALUES FROM ('2025-11-01') TO ('2025-12-01');
123
CREATE TABLE smart_param_values_2025_12 PARTITION OF smart_param_values
124
    FOR VALUES FROM ('2025-12-01') TO ('2026-01-01');
125
CREATE TABLE smart_param_values_2026_01 PARTITION OF smart_param_values
126
    FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
127
CREATE TABLE smart_param_values_2026_02 PARTITION OF smart_param_values
128
    FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
129
CREATE TABLE smart_param_values_2026_03 PARTITION OF smart_param_values
130
    FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
131
CREATE TABLE smart_param_values_2026_04 PARTITION OF smart_param_values
132
    FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
133
CREATE TABLE smart_param_values_2026_05 PARTITION OF smart_param_values
134
    FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
135
CREATE TABLE smart_param_values_2026_06 PARTITION OF smart_param_values
136
    FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
137
CREATE TABLE smart_param_values_2026_07 PARTITION OF smart_param_values
138
    FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
139
CREATE TABLE smart_param_values_2026_08 PARTITION OF smart_param_values
140
    FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');
141
CREATE TABLE smart_param_values_2026_09 PARTITION OF smart_param_values
142
    FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
143
CREATE TABLE smart_param_values_2026_10 PARTITION OF smart_param_values
144
    FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
145
CREATE TABLE smart_param_values_2026_11 PARTITION OF smart_param_values
146
    FOR VALUES FROM ('2026-11-01') TO ('2026-12-01');
147
CREATE TABLE smart_param_values_2026_12 PARTITION OF smart_param_values
148
    FOR VALUES FROM ('2026-12-01') TO ('2027-01-01');
149

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

            
153
-- Indexuri pe partiții
154
CREATE INDEX idx_spv_hdd_param_time ON smart_param_values(hdd_id, param_id, collected_at DESC);
155
CREATE INDEX idx_spv_event_id ON smart_param_values(event_id);
156
CREATE INDEX idx_spv_param_time ON smart_param_values(param_id, collected_at DESC);
157
CREATE INDEX idx_spv_critical_values ON smart_param_values(hdd_id, collected_at DESC)
158
    WHERE raw_value > 0;
159

            
160
-- ============================================================================
161
-- VIEWS
162
-- ============================================================================
163

            
164
-- View 1: Ultimele valori per disc per parametru
165
CREATE VIEW v_latest_param_values AS
166
SELECT DISTINCT ON (spv.hdd_id, spv.param_id)
167
    spv.hdd_id,
168
    spv.param_id,
169
    spc.param_name,
170
    spc.unit,
171
    spc.is_critical,
172
    spv.raw_value,
173
    spv.normalized_value,
174
    spv.collected_at,
175
    hi.serial_number,
176
    hi.model_name,
177
    hi.current_node_id
178
FROM smart_param_values spv
179
JOIN smart_param_catalog spc ON spv.param_id = spc.id
180
JOIN hdd_inventory hi ON spv.hdd_id = hi.id
181
WHERE hi.status = 'active'
182
ORDER BY spv.hdd_id, spv.param_id, spv.collected_at DESC;
183

            
184
-- View 2: Starea curentă a fiecărui disc
185
CREATE VIEW v_drive_health_summary AS
186
SELECT
187
    hi.id              AS hdd_id,
188
    hi.serial_number,
189
    hi.model_name,
190
    hi.manufacturer,
191
    hi.current_node_id,
192
    hi.current_device_path,
193
    hi.status,
194
    hi.size_gb,
195
    sce_last.collected_at   AS last_collection,
196
    sce_last.temperature    AS last_temperature,
197
    sce_last.collection_ok  AS last_collection_ok,
198
    EXTRACT(EPOCH FROM (NOW() - sce_last.collected_at)) / 3600 AS hours_since_collection,
199
    critical_counts.params_above_zero AS critical_params_triggered,
200
    pred.risk_level,
201
    pred.failure_probability,
202
    pred.predicted_failure_date
203
FROM hdd_inventory hi
204
LEFT JOIN LATERAL (
205
    SELECT collected_at, temperature, collection_ok
206
    FROM smart_collection_events
207
    WHERE hdd_id = hi.id
208
    ORDER BY collected_at DESC
209
    LIMIT 1
210
) sce_last ON true
211
LEFT JOIN LATERAL (
212
    SELECT COUNT(*) AS params_above_zero
213
    FROM smart_param_values spv
214
    JOIN smart_param_catalog spc ON spv.param_id = spc.id
215
    WHERE spv.hdd_id = hi.id
216
      AND spc.is_critical = true
217
      AND spv.raw_value > 0
218
      AND spv.collected_at >= COALESCE((
219
          SELECT collected_at FROM smart_collection_events
220
          WHERE hdd_id = hi.id ORDER BY collected_at DESC LIMIT 1
221
      ), NOW()) - INTERVAL '5 minutes'
222
) critical_counts ON true
223
LEFT JOIN LATERAL (
224
    SELECT risk_level, failure_probability, predicted_failure_date
225
    FROM predictions
226
    WHERE hdd_id = hi.id
227
    ORDER BY timestamp DESC
228
    LIMIT 1
229
) pred ON true
230
WHERE hi.status = 'active';
231

            
232
-- View 3: Trending parametru specific
233
CREATE VIEW v_param_trend AS
234
SELECT
235
    hi.serial_number,
236
    hi.model_name,
237
    hi.current_node_id,
238
    spc.param_name,
239
    spc.unit,
240
    spc.is_critical,
241
    spc.warning_threshold,
242
    spc.critical_threshold,
243
    spv.raw_value,
244
    spv.normalized_value,
245
    spv.collected_at,
246
    sce.node_id AS collected_on_node,
247
    sce.temperature AS drive_temp_at_collection
248
FROM smart_param_values spv
249
JOIN smart_collection_events sce ON spv.event_id = sce.id
250
JOIN smart_param_catalog spc ON spv.param_id = spc.id
251
JOIN hdd_inventory hi ON spv.hdd_id = hi.id;
252

            
253
-- View 4: Cluster overview
254
CREATE VIEW v_cluster_overview AS
255
SELECT
256
    hi.current_node_id AS node,
257
    COUNT(*) AS total_drives,
258
    COUNT(*) FILTER (WHERE dhs.last_collection > NOW() - INTERVAL '1 hour') AS drives_recently_collected,
259
    COUNT(*) FILTER (WHERE dhs.critical_params_triggered > 0) AS drives_with_critical_params,
260
    COUNT(*) FILTER (WHERE dhs.risk_level IN ('high', 'critical')) AS drives_high_risk,
261
    MAX(dhs.last_collection) AS latest_collection,
262
    MIN(dhs.last_collection) AS oldest_collection
263
FROM hdd_inventory hi
264
JOIN v_drive_health_summary dhs ON hi.id = dhs.hdd_id
265
WHERE hi.status = 'active'
266
GROUP BY hi.current_node_id;
267

            
268
-- View 5: Compatibilitate cu codul existent (emulează smart_readings)
269
CREATE VIEW v_smart_readings_compat AS
270
SELECT
271
    sce.id,
272
    sce.hdd_id,
273
    sce.serial_number,
274
    sce.collected_at AS timestamp,
275
    sce.temperature,
276
    sce.node_id,
277
    sce.collection_ok,
278
    jsonb_object_agg(
279
        COALESCE(spc.param_name, ''),
280
        spv.raw_value
281
    ) FILTER (WHERE spc.param_name IS NOT NULL) AS parameters_json
282
FROM smart_collection_events sce
283
LEFT JOIN smart_param_values spv ON spv.event_id = sce.id
284
LEFT JOIN smart_param_catalog spc ON spv.param_id = spc.id
285
GROUP BY sce.id, sce.hdd_id, sce.serial_number, sce.collected_at, sce.temperature, sce.node_id, sce.collection_ok;
286

            
287
-- ============================================================================
288
-- FUNCTIONS
289
-- ============================================================================
290

            
291
-- Upsert parametru în catalog (auto-discovery)
292
CREATE OR REPLACE FUNCTION upsert_param_catalog(
293
    p_param_name    VARCHAR(100),
294
    p_param_id_ata  INTEGER DEFAULT NULL,
295
    p_unit          VARCHAR(20) DEFAULT 'count'
296
) RETURNS INTEGER AS $$
297
DECLARE
298
    v_id INTEGER;
299
BEGIN
300
    INSERT INTO smart_param_catalog (param_name, param_id_ata, unit, first_seen)
301
    VALUES (p_param_name, p_param_id_ata, p_unit, NOW())
302
    ON CONFLICT (param_name) DO UPDATE
303
        SET param_id_ata = COALESCE(EXCLUDED.param_id_ata, smart_param_catalog.param_id_ata),
304
            updated_at = NOW()
305
    RETURNING id INTO v_id;
306
    RETURN v_id;
307
END;
308
$$ LANGUAGE plpgsql;
309

            
310
-- Inserare atomică event + valori (înlocuiește logica de collection)
311
CREATE OR REPLACE FUNCTION insert_collection_event(
312
    p_hdd_id        INTEGER,
313
    p_serial        VARCHAR(100),
314
    p_node_id       VARCHAR(50),
315
    p_collected_at  TIMESTAMPTZ,
316
    p_temperature   SMALLINT,
317
    p_collection_ok BOOLEAN,
318
    p_checksum      VARCHAR(64),
319
    p_params        JSONB
320
) RETURNS BIGINT AS $$
321
DECLARE
322
    v_event_id  BIGINT;
323
    v_param_id  INTEGER;
324
    v_param     RECORD;
325
    v_count     SMALLINT := 0;
326
BEGIN
327
    INSERT INTO smart_collection_events
328
        (hdd_id, serial_number, node_id, collected_at,
329
         collection_ok, temperature, checksum, param_count)
330
    VALUES
331
        (p_hdd_id, p_serial, p_node_id, p_collected_at,
332
         p_collection_ok, p_temperature, p_checksum, 0)
333
    RETURNING id INTO v_event_id;
334

            
335
    FOR v_param IN SELECT * FROM jsonb_each(p_params)
336
    LOOP
337
        v_param_id := upsert_param_catalog(
338
            v_param.key,
339
            (v_param.value->>'id')::INTEGER,
340
            'count'
341
        );
342

            
343
        INSERT INTO smart_param_values
344
            (event_id, hdd_id, param_id, collected_at,
345
             raw_value, normalized_value, worst_value, threshold_value, when_failed)
346
        VALUES
347
            (v_event_id, p_hdd_id, v_param_id, p_collected_at,
348
             (v_param.value->>'raw_value')::BIGINT,
349
             (v_param.value->>'value')::SMALLINT,
350
             (v_param.value->>'worst')::SMALLINT,
351
             (v_param.value->>'thresh')::SMALLINT,
352
             v_param.value->>'when_failed');
353

            
354
        v_count := v_count + 1;
355
    END LOOP;
356

            
357
    UPDATE smart_collection_events SET param_count = v_count WHERE id = v_event_id;
358

            
359
    RETURN v_event_id;
360
END;
361
$$ LANGUAGE plpgsql;
362

            
363
-- Creare partiție lunară (cu idempotență)
364
CREATE OR REPLACE FUNCTION create_monthly_partition(p_year INTEGER, p_month INTEGER)
365
RETURNS VOID AS $$
366
DECLARE
367
    v_table_name    TEXT;
368
    v_start_date    DATE;
369
    v_end_date      DATE;
370
BEGIN
371
    v_table_name := format('smart_param_values_%s_%s',
372
        p_year, lpad(p_month::TEXT, 2, '0'));
373
    v_start_date := make_date(p_year, p_month, 1);
374
    v_end_date   := v_start_date + INTERVAL '1 month';
375

            
376
    IF NOT EXISTS (
377
        SELECT 1 FROM information_schema.tables
378
        WHERE table_name = v_table_name AND table_schema = 'public'
379
    ) THEN
380
        EXECUTE format(
381
            'CREATE TABLE %I PARTITION OF smart_param_values
382
             FOR VALUES FROM (%L) TO (%L)',
383
            v_table_name, v_start_date, v_end_date
384
        );
385
        RAISE NOTICE 'Created partition: %', v_table_name;
386
    END IF;
387
END;
388
$$ LANGUAGE plpgsql;
389

            
Bogdan Timofte authored 2 weeks ago
390
-- On-demand data deletion: delete all data for a specific HDD (by serial_number)
391
-- RETENTION POLICY: On-demand only (no automatic cleanup)
392
-- Called from frontend when user requests to remove a drive from monitoring
393
CREATE OR REPLACE FUNCTION delete_hdd_data_by_serial(
394
    p_serial_number VARCHAR(100),
395
    p_keep_catalog  BOOLEAN DEFAULT true
396
) RETURNS TABLE(
397
    deleted_values BIGINT,
398
    deleted_events BIGINT,
399
    deleted_catalog INTEGER
400
) AS $$
Bogdan Timofte authored 2 weeks ago
401
DECLARE
Bogdan Timofte authored 2 weeks ago
402
    v_hdd_id            INTEGER;
403
    v_deleted_values    BIGINT := 0;
404
    v_deleted_events    BIGINT := 0;
405
    v_deleted_catalog   INTEGER := 0;
Bogdan Timofte authored 2 weeks ago
406
BEGIN
Bogdan Timofte authored 2 weeks ago
407
    -- Get HDD ID for this serial
408
    SELECT id INTO v_hdd_id FROM hdd_inventory WHERE serial_number = p_serial_number;
Bogdan Timofte authored 2 weeks ago
409

            
Bogdan Timofte authored 2 weeks ago
410
    IF v_hdd_id IS NULL THEN
411
        RAISE NOTICE 'Serial % not found in inventory', p_serial_number;
412
        RETURN QUERY SELECT 0::BIGINT, 0::BIGINT, 0::INTEGER;
413
        RETURN;
414
    END IF;
415

            
416
    -- Delete from smart_param_values (cascade via event_id FK)
417
    DELETE FROM smart_param_values spv
418
    WHERE event_id IN (
419
        SELECT id FROM smart_collection_events
420
        WHERE hdd_id = v_hdd_id
421
    );
422
    GET DIAGNOSTICS v_deleted_values = ROW_COUNT;
Bogdan Timofte authored 2 weeks ago
423

            
Bogdan Timofte authored 2 weeks ago
424
    -- Delete from smart_collection_events
Bogdan Timofte authored 2 weeks ago
425
    DELETE FROM smart_collection_events
Bogdan Timofte authored 2 weeks ago
426
    WHERE hdd_id = v_hdd_id OR serial_number = p_serial_number;
427
    GET DIAGNOSTICS v_deleted_events = ROW_COUNT;
428

            
429
    -- Optionally delete from hdd_inventory and catalog
430
    IF NOT p_keep_catalog THEN
431
        DELETE FROM smart_param_catalog spc
432
        WHERE NOT EXISTS (
433
            SELECT 1 FROM smart_param_values
434
            WHERE param_id = spc.id
435
        );
436
        GET DIAGNOSTICS v_deleted_catalog = ROW_COUNT;
437

            
438
        DELETE FROM hdd_inventory WHERE id = v_hdd_id;
439
    END IF;
440

            
441
    RAISE NOTICE 'Deleted % param values, % events for serial %',
442
        v_deleted_values, v_deleted_events, p_serial_number;
Bogdan Timofte authored 2 weeks ago
443

            
Bogdan Timofte authored 2 weeks ago
444
    RETURN QUERY SELECT v_deleted_values, v_deleted_events, v_deleted_catalog;
Bogdan Timofte authored 2 weeks ago
445
END;
446
$$ LANGUAGE plpgsql;
447

            
448
-- ============================================================================
449
-- PERMISSIONS
450
-- ============================================================================
451

            
452
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO autosmart;
453
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO autosmart;
454
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO autosmart;
455

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

            
462
-- ============================================================================
463
-- FINAL MESSAGE
464
-- ============================================================================
465

            
466
DO $$
467
BEGIN
468
    RAISE NOTICE '✅ autoSMART Schema v2.0 deployed successfully!';
469
    RAISE NOTICE 'New tables: smart_param_catalog, smart_collection_events, smart_param_values (partitioned)';
470
    RAISE NOTICE 'Views: v_latest_param_values, v_drive_health_summary, v_param_trend, v_cluster_overview, v_smart_readings_compat';
Bogdan Timofte authored 2 weeks ago
471
    RAISE NOTICE 'Functions: upsert_param_catalog, insert_collection_event, create_monthly_partition, delete_hdd_data_by_serial';
Bogdan Timofte authored 2 weeks ago
472
    RAISE NOTICE 'Next step: Run sql/migrate-v1-to-v2.sql to migrate existing data';
473
END $$;