HH-441: harden durable storage and recovery (#92)
* HH-441: harden durable storage and recovery * HH-441: clear recovery review blockers * HH-441: enforce offsite backup failure domain --------- Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
@@ -1,22 +1,5 @@
|
||||
-- Revert reporting_events_rollups to the original (incorrect) init schema structure.
|
||||
-- WARNING: this restores the broken column names (dimension, dimension_value, metric_name,
|
||||
-- value, value_in_business_hours, period) that do not match the GORM model.
|
||||
|
||||
DROP TABLE IF EXISTS reporting_events_rollups;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS reporting_events_rollups (
|
||||
id SERIAL PRIMARY KEY,
|
||||
account_id INTEGER NOT NULL,
|
||||
dimension VARCHAR(50) NOT NULL,
|
||||
dimension_value VARCHAR(255) NOT NULL,
|
||||
metric_name VARCHAR(50) NOT NULL,
|
||||
value DOUBLE PRECISION NOT NULL,
|
||||
value_in_business_hours DOUBLE PRECISION DEFAULT 0,
|
||||
period VARCHAR(50) NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
UNIQUE(account_id, dimension, dimension_value, metric_name, period)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_reporting_events_rollups_deleted_at ON reporting_events_rollups(deleted_at);
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'migration 000048 is irreversible; restore the pre-upgrade backup instead of destroying rollup data';
|
||||
END
|
||||
$$;
|
||||
|
||||
@@ -1,18 +1,67 @@
|
||||
-- Fix reporting_events_rollups table structure to match GORM model
|
||||
-- The init schema (000001) created columns: dimension, dimension_value, metric_name,
|
||||
-- value, value_in_business_hours, period — but the GORM model and all Go code use:
|
||||
-- date, dimension_type, dimension_id, metric, count, sum_value, sum_value_business_hours.
|
||||
-- This migration drops and recreates the table with the correct schema.
|
||||
-- The table is empty in practice (rollups are computed on-demand), so data loss is negligible.
|
||||
-- Convert the legacy rollup schema without dropping its data. The explicit
|
||||
-- transaction makes lock/statement timeout failures leave the old table intact.
|
||||
BEGIN;
|
||||
LOCK TABLE reporting_events_rollups IN ACCESS EXCLUSIVE MODE;
|
||||
|
||||
DROP TABLE IF EXISTS reporting_events_rollups;
|
||||
DO $$
|
||||
DECLARE
|
||||
invalid_periods BIGINT;
|
||||
invalid_dimensions BIGINT;
|
||||
invalid_counts BIGINT;
|
||||
oversized_dimensions BIGINT;
|
||||
collision_groups BIGINT;
|
||||
BEGIN
|
||||
SELECT count(*) INTO invalid_periods
|
||||
FROM reporting_events_rollups
|
||||
WHERE NOT pg_input_is_valid(period, 'date');
|
||||
IF invalid_periods > 0 THEN
|
||||
RAISE EXCEPTION 'migration 000048: % rollup period value(s) cannot convert to date', invalid_periods
|
||||
USING HINT = 'Repair or remove the reported legacy rows explicitly, then retry the migration.';
|
||||
END IF;
|
||||
|
||||
CREATE TABLE reporting_events_rollups (
|
||||
SELECT count(*) INTO invalid_dimensions
|
||||
FROM reporting_events_rollups
|
||||
WHERE NOT pg_input_is_valid(dimension_value, 'bigint');
|
||||
IF invalid_dimensions > 0 THEN
|
||||
RAISE EXCEPTION 'migration 000048: % rollup dimension_value(s) cannot convert to bigint', invalid_dimensions
|
||||
USING HINT = 'Map non-numeric legacy dimensions explicitly, then retry the migration.';
|
||||
END IF;
|
||||
|
||||
SELECT count(*) INTO invalid_counts
|
||||
FROM reporting_events_rollups
|
||||
WHERE metric_name LIKE '%count%' AND NOT pg_input_is_valid(value::text, 'bigint');
|
||||
IF invalid_counts > 0 THEN
|
||||
RAISE EXCEPTION 'migration 000048: % count value(s) cannot convert losslessly to bigint', invalid_counts
|
||||
USING HINT = 'Repair fractional, non-finite, or out-of-range count values, then retry the migration.';
|
||||
END IF;
|
||||
|
||||
SELECT count(*) INTO oversized_dimensions
|
||||
FROM reporting_events_rollups
|
||||
WHERE length(dimension) > 20;
|
||||
IF oversized_dimensions > 0 THEN
|
||||
RAISE EXCEPTION 'migration 000048: % dimension value(s) exceed the new 20-character limit', oversized_dimensions
|
||||
USING HINT = 'Shorten or explicitly map oversized dimensions, then retry the migration.';
|
||||
END IF;
|
||||
|
||||
SELECT count(*) INTO collision_groups
|
||||
FROM (
|
||||
SELECT 1
|
||||
FROM reporting_events_rollups
|
||||
GROUP BY account_id, period::date, dimension, dimension_value::BIGINT, metric_name
|
||||
HAVING count(*) > 1
|
||||
) collisions;
|
||||
IF collision_groups > 0 THEN
|
||||
RAISE EXCEPTION 'migration 000048: % unique-key collision group(s) appear after type conversion', collision_groups
|
||||
USING HINT = 'Merge or choose one legacy row in each collision group explicitly, then retry the migration.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE TABLE reporting_events_rollups_v2 (
|
||||
id SERIAL PRIMARY KEY,
|
||||
account_id INTEGER NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
dimension_type VARCHAR(20) NOT NULL,
|
||||
dimension_id INTEGER NOT NULL,
|
||||
dimension_id BIGINT NOT NULL,
|
||||
metric VARCHAR(50) NOT NULL,
|
||||
count BIGINT NOT NULL DEFAULT 0,
|
||||
sum_value DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
@@ -23,7 +72,36 @@ CREATE TABLE reporting_events_rollups (
|
||||
UNIQUE(account_id, date, dimension_type, dimension_id, metric)
|
||||
);
|
||||
|
||||
INSERT INTO reporting_events_rollups_v2 (
|
||||
id, account_id, date, dimension_type, dimension_id, metric, count,
|
||||
sum_value, sum_value_business_hours, created_at, updated_at, deleted_at
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
account_id,
|
||||
period::date,
|
||||
dimension,
|
||||
dimension_value::BIGINT,
|
||||
metric_name,
|
||||
CASE WHEN metric_name LIKE '%count%' THEN value::BIGINT ELSE 1 END,
|
||||
value,
|
||||
COALESCE(value_in_business_hours, 0),
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at
|
||||
FROM reporting_events_rollups;
|
||||
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('reporting_events_rollups_v2', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM reporting_events_rollups_v2), 1),
|
||||
EXISTS (SELECT 1 FROM reporting_events_rollups_v2)
|
||||
);
|
||||
|
||||
DROP TABLE reporting_events_rollups;
|
||||
ALTER TABLE reporting_events_rollups_v2 RENAME TO reporting_events_rollups;
|
||||
ALTER INDEX reporting_events_rollups_v2_pkey RENAME TO reporting_events_rollups_pkey;
|
||||
CREATE INDEX idx_reporting_events_rollups_deleted_at ON reporting_events_rollups(deleted_at);
|
||||
CREATE INDEX idx_reporting_events_rollups_account_id ON reporting_events_rollups(account_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_reporting_events_rollups_date ON reporting_events_rollups(date) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_reporting_events_rollups_dimension ON reporting_events_rollups(dimension_type, dimension_id) WHERE deleted_at IS NULL;
|
||||
COMMIT;
|
||||
|
||||
+4
-52
@@ -1,52 +1,4 @@
|
||||
DELETE FROM custom_attribute_definitions
|
||||
WHERE attribute_model = 'conversation_attribute'
|
||||
AND attribute_name IN (
|
||||
'swt_source_url',
|
||||
'swt_source_search_term',
|
||||
'swt_source_purchase_term',
|
||||
'swt_source_keyword_id',
|
||||
'swt_source_channel',
|
||||
'swt_source_realtime_location',
|
||||
'swt_source_region',
|
||||
'swt_source_ad_account_id',
|
||||
'swt_source_wakeable',
|
||||
'swt_baidu_conversation_type',
|
||||
'swt_baidu_agent_name',
|
||||
'swt_baidu_ssid'
|
||||
);
|
||||
|
||||
UPDATE conversations
|
||||
SET
|
||||
custom_attributes = COALESCE(custom_attributes, '{}'::jsonb)
|
||||
- 'swt_source_url'
|
||||
- 'swt_source_description'
|
||||
- 'swt_source_search_term'
|
||||
- 'swt_source_purchase_term'
|
||||
- 'swt_source_keyword_id'
|
||||
- 'swt_source_channel'
|
||||
- 'swt_source_realtime_location'
|
||||
- 'swt_source_region'
|
||||
- 'swt_source_ad_account_id'
|
||||
- 'swt_source_wakeable'
|
||||
- 'swt_baidu_conversation_type'
|
||||
- 'swt_baidu_agent_name'
|
||||
- 'swt_baidu_ssid',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE COALESCE(custom_attributes, '{}'::jsonb) ?| ARRAY[
|
||||
'swt_source_url',
|
||||
'swt_source_description',
|
||||
'swt_source_search_term',
|
||||
'swt_source_purchase_term',
|
||||
'swt_source_keyword_id',
|
||||
'swt_source_channel',
|
||||
'swt_source_realtime_location',
|
||||
'swt_source_region',
|
||||
'swt_source_ad_account_id',
|
||||
'swt_source_wakeable',
|
||||
'swt_baidu_conversation_type',
|
||||
'swt_baidu_agent_name',
|
||||
'swt_baidu_ssid'
|
||||
];
|
||||
|
||||
-- Historical kind=8 system messages are backfilled from the Connector SQLite
|
||||
-- store because PostgreSQL intentionally does not retain the raw encoded payload.
|
||||
-- Keep the legacy source attributes during the expand/contract window. New
|
||||
-- connector versions emit source information as system messages, while an old
|
||||
-- application digest still needs these definitions and values after rollback.
|
||||
SELECT 1;
|
||||
|
||||
Reference in New Issue
Block a user