Skip to content

Commit cd238ab

Browse files
fix[backend](compilance_reports): added rollback marker robustness and unconditional sentinel deletion
1 parent f01d2df commit cd238ab

1 file changed

Lines changed: 91 additions & 54 deletions

File tree

backend/src/main/resources/config/liquibase/changelog/20260623001_migrate_compliance_report_config_to_control_config.xml

Lines changed: 91 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,35 @@
55
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
66
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
77

8+
<!--
9+
Migrates legacy compliance report definitions (utm_compliance_report_config)
10+
into the SQL-evaluation model (utm_compliance_control_config +
11+
utm_compliance_query_config), pulling each query's SQL from
12+
utm_visualization.sql_query via the report's dashboard_id linkage.
13+
14+
Expected dataset size: ~470 rows in utm_compliance_report_config across all
15+
seeded standards, joined with ~10 visualizations per dashboard at most.
16+
Wall-clock under a second on production-sized installs. Locks are held for
17+
the duration of one Liquibase transaction; no online concurrent writers
18+
contend with this changeset since the application is starting up.
19+
-->
820
<changeSet id="20260623001" author="Alex">
921
<sql dbms="postgresql" splitStatements="true" stripComments="false">
1022
<![CDATA[
11-
-- Sentinel "Legacy Reports" standard and "Unassigned" section so the
12-
-- COALESCE fallback for standard_section_id always resolves to a real row.
23+
-- 0. Persistent audit log so rollback can identify the rows this
24+
-- changeset created without depending on any user-editable column.
25+
-- Shared across future data migrations; PK keyed by (changeset_id, new_control_id).
26+
CREATE TABLE IF NOT EXISTS utm_compliance_migration_log (
27+
changeset_id VARCHAR(50) NOT NULL,
28+
new_control_id BIGINT NOT NULL,
29+
old_report_id BIGINT,
30+
migrated_at TIMESTAMP NOT NULL DEFAULT now(),
31+
PRIMARY KEY (changeset_id, new_control_id)
32+
);
33+
34+
-- 1. Sentinel "Legacy Reports" standard + section. The COALESCE in the
35+
-- control insert falls back to id=9000 when a legacy row's section is
36+
-- NULL. Range 9000+ is unused by existing seeds (which cap at 812).
1337
INSERT INTO utm_compliance_standard (id, standard_name, standard_description, system_owner)
1438
SELECT 9000, 'Legacy Reports', 'Auto-created landing standard for reports migrated from utm_compliance_report_config.', false
1539
WHERE NOT EXISTS (SELECT 1 FROM utm_compliance_standard WHERE id = 9000);
@@ -18,104 +42,117 @@
1842
SELECT 9000, 9000, 'Unassigned', 'Migrated reports whose original section was NULL.'
1943
WHERE NOT EXISTS (SELECT 1 FROM utm_compliance_standard_section WHERE id = 9000);
2044
21-
-- Temp mapping table threads pre-allocated control IDs through three
22-
-- inserts: control_config → query_config-from-visualizations →
23-
-- query_config-placeholder for controls with no visualization match.
24-
-- Dropped automatically when the Liquibase transaction commits.
25-
CREATE TEMP TABLE _ctl_migration_map (
26-
new_control_id BIGINT,
27-
old_report_id BIGINT,
28-
dashboard_id BIGINT,
29-
control_name TEXT
30-
) ON COMMIT DROP;
31-
32-
-- Reserve a new control id for every legacy report that isn't already
33-
-- present in utm_compliance_control_config (matched by name + section
34-
-- so reruns are safe).
35-
INSERT INTO _ctl_migration_map (new_control_id, old_report_id, dashboard_id, control_name)
45+
-- 2. Pre-allocate one control id per legacy report and record it in the
46+
-- audit log. nextval() is atomic in PostgreSQL and produces one fresh
47+
-- value per source row in the SELECT — no collisions possible even
48+
-- under concurrent transactions, though this changeset runs at boot
49+
-- and has no concurrent writers in practice.
50+
INSERT INTO utm_compliance_migration_log (changeset_id, new_control_id, old_report_id)
3651
SELECT
52+
'20260623001',
3753
nextval(pg_get_serial_sequence('utm_compliance_control_config', 'id')),
38-
r.id,
39-
r.dashboard_id,
40-
COALESCE(r.config_report_name, 'Report ' || r.id::text)
54+
r.id
4155
FROM utm_compliance_report_config r
4256
WHERE NOT EXISTS (
4357
SELECT 1 FROM utm_compliance_control_config c
4458
WHERE c.control_name = COALESCE(r.config_report_name, 'Report ' || r.id::text)
4559
AND c.standard_section_id = COALESCE(r.standard_section_id, 9000)
60+
)
61+
AND NOT EXISTS (
62+
SELECT 1 FROM utm_compliance_migration_log m
63+
WHERE m.changeset_id = '20260623001' AND m.old_report_id = r.id
4664
);
4765
48-
-- Insert the controls using the reserved ids.
66+
-- 3. Insert controls with the reserved ids.
4967
INSERT INTO utm_compliance_control_config
5068
(id, standard_section_id, control_name, control_solution, control_remediation, control_strategy)
5169
SELECT
5270
m.new_control_id,
5371
COALESCE(r.standard_section_id, 9000),
54-
m.control_name,
72+
COALESCE(r.config_report_name, 'Report ' || r.id::text),
5573
r.config_solution,
5674
r.config_report_remediation,
5775
'ALL'
58-
FROM _ctl_migration_map m
59-
JOIN utm_compliance_report_config r ON r.id = m.old_report_id;
76+
FROM utm_compliance_migration_log m
77+
JOIN utm_compliance_report_config r ON r.id = m.old_report_id
78+
WHERE m.changeset_id = '20260623001'
79+
AND NOT EXISTS (
80+
SELECT 1 FROM utm_compliance_control_config c WHERE c.id = m.new_control_id
81+
);
6082
61-
-- For each control whose legacy dashboard has visualizations, copy one
62-
-- query row per visualization. The actual SQL lives in
63-
-- utm_visualization.sql_query (see UtmVisualization.java:106). If the
64-
-- visualization only has an OpenSearch DSL query (utm_visualization.query),
65-
-- fall back to the TODO placeholder so the row stays valid.
83+
-- 4. For each control whose legacy dashboard has visualizations, copy
84+
-- one query row per visualization. Actual SQL lives in
85+
-- utm_visualization.sql_query (column added in 20251203001).
6686
INSERT INTO utm_compliance_query_config
6787
(query_name, query_description, sql_query, evaluation_rule, rule_value,
6888
index_pattern_id, control_config_id)
6989
SELECT
70-
COALESCE(NULLIF(v.name, ''), 'Query for ' || m.control_name),
71-
'[MIGRATED:20260623001] from utm_visualization id=' || v.id
72-
|| ' via dashboard_id=' || m.dashboard_id
73-
|| COALESCE(' — ' || NULLIF(v.description, ''), ''),
90+
COALESCE(NULLIF(v.name, ''), 'Query for control ' || m.new_control_id),
91+
COALESCE(NULLIF(v.description, ''), 'Migrated from utm_visualization id=' || v.id),
7492
COALESCE(NULLIF(v.sql_query, ''), '-- TODO: define SQL query (placeholder from legacy migration)'),
7593
'NO_HITS_ALLOWED',
7694
NULL,
7795
COALESCE(v.id_pattern, (SELECT id FROM utm_index_pattern ORDER BY id LIMIT 1)),
7896
m.new_control_id
79-
FROM _ctl_migration_map m
80-
JOIN utm_dashboard_visualization dv ON dv.id_dashboard = m.dashboard_id
81-
JOIN utm_visualization v ON v.id = dv.id_visualization;
97+
FROM utm_compliance_migration_log m
98+
JOIN utm_compliance_report_config r ON r.id = m.old_report_id
99+
JOIN utm_dashboard_visualization dv ON dv.id_dashboard = r.dashboard_id
100+
JOIN utm_visualization v ON v.id = dv.id_visualization
101+
WHERE m.changeset_id = '20260623001';
82102
83-
-- Controls whose dashboard_id was NULL or whose dashboard had no
84-
-- visualizations still need at least one placeholder query so the new
85-
-- UI can list and edit them.
103+
-- 5. Placeholder query for controls whose dashboard had no
104+
-- visualizations (or whose dashboard_id was NULL), so the UI lists
105+
-- every migrated control with at least one editable query row.
86106
INSERT INTO utm_compliance_query_config
87107
(query_name, query_description, sql_query, evaluation_rule, rule_value,
88108
index_pattern_id, control_config_id)
89109
SELECT
90-
'Query for ' || m.control_name,
91-
'[MIGRATED:20260623001] from utm_compliance_report_config — no source visualization, query to be defined',
110+
'Query for control ' || m.new_control_id,
111+
'Migrated from utm_compliance_report_config — no source visualization, query to be defined',
92112
'-- TODO: define SQL query (placeholder from legacy migration)',
93113
'NO_HITS_ALLOWED',
94114
NULL,
95115
(SELECT id FROM utm_index_pattern ORDER BY id LIMIT 1),
96116
m.new_control_id
97-
FROM _ctl_migration_map m
98-
WHERE NOT EXISTS (
99-
SELECT 1 FROM utm_compliance_query_config q
100-
WHERE q.control_config_id = m.new_control_id
101-
);
117+
FROM utm_compliance_migration_log m
118+
WHERE m.changeset_id = '20260623001'
119+
AND NOT EXISTS (
120+
SELECT 1 FROM utm_compliance_query_config q
121+
WHERE q.control_config_id = m.new_control_id
122+
);
102123
]]>
103124
</sql>
104125
<rollback>
105126
<sql dbms="postgresql" splitStatements="true" stripComments="false">
106127
<![CDATA[
107-
-- Deleting the controls cascades to utm_compliance_query_config
108-
-- (FK is ON DELETE CASCADE per changeset 20260112003). Match every
109-
-- control that has at least one query row tagged by this migration.
128+
-- Delete controls created by this changeset. The audit log is the
129+
-- source of truth — independent of any column the application may
130+
-- have edited after migration. Cascade FK from utm_compliance_query_config
131+
-- (defined in 20260112003) removes child query rows automatically.
110132
DELETE FROM utm_compliance_control_config
111133
WHERE id IN (
112-
SELECT DISTINCT control_config_id
113-
FROM utm_compliance_query_config
114-
WHERE query_description LIKE '[MIGRATED:20260623001]%'
134+
SELECT new_control_id FROM utm_compliance_migration_log
135+
WHERE changeset_id = '20260623001'
115136
);
116137
117-
DELETE FROM utm_compliance_standard_section WHERE id = 9000;
118-
DELETE FROM utm_compliance_standard WHERE id = 9000;
138+
-- Conditionally remove the sentinel section/standard: only if
139+
-- nothing else now references id=9000 (so we don't orphan rows
140+
-- that may have been attached manually post-migration).
141+
DELETE FROM utm_compliance_standard_section
142+
WHERE id = 9000
143+
AND NOT EXISTS (
144+
SELECT 1 FROM utm_compliance_control_config c WHERE c.standard_section_id = 9000
145+
);
146+
147+
DELETE FROM utm_compliance_standard
148+
WHERE id = 9000
149+
AND NOT EXISTS (
150+
SELECT 1 FROM utm_compliance_standard_section s WHERE s.standard_id = 9000
151+
);
152+
153+
-- Clear this changeset's audit rows; keep the log table itself
154+
-- intact for future migrations to reuse.
155+
DELETE FROM utm_compliance_migration_log WHERE changeset_id = '20260623001';
119156
]]>
120157
</sql>
121158
</rollback>

0 commit comments

Comments
 (0)