Skip to content

Commit f10ecb1

Browse files
committed
Implement canonical report runs and legacy ID map
1 parent 11fcff3 commit f10ecb1

15 files changed

Lines changed: 1628 additions & 328 deletions

changelog.d/3500.changed.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Preserve explicit report definitions and execution metadata across later syncs, key new report creation and alias validation by canonical report identity, and resolve report reads through canonical parents plus display-run selection.
1+
Preserve explicit report definitions and execution metadata, key report creation by canonical report identity, resolve legacy report IDs through a permanent compatibility map, and add run-targeted report and simulation rerun updates.

policyengine_api/data/initialise.sql

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ CREATE TABLE IF NOT EXISTS report_outputs (
141141
latest_successful_run_id CHAR(36) DEFAULT NULL
142142
);
143143

144+
CREATE INDEX report_outputs_identity_idx
145+
ON report_outputs (
146+
country_id, report_identity_hash, report_identity_schema_version
147+
);
148+
144149
CREATE TABLE IF NOT EXISTS report_output_runs (
145150
id CHAR(36) PRIMARY KEY,
146151
report_output_id INT NOT NULL,
@@ -189,7 +194,13 @@ CREATE TABLE IF NOT EXISTS simulation_runs (
189194
UNIQUE KEY simulation_run_sequence_idx (simulation_id, run_sequence)
190195
);
191196

192-
CREATE TABLE IF NOT EXISTS legacy_report_output_aliases (
197+
CREATE INDEX simulation_runs_report_output_run_idx
198+
ON simulation_runs (report_output_run_id);
199+
200+
CREATE TABLE IF NOT EXISTS legacy_report_output_id_map (
193201
legacy_report_output_id INT PRIMARY KEY,
194202
canonical_report_output_id INT NOT NULL
195203
);
204+
205+
CREATE INDEX legacy_report_output_id_map_canonical_idx
206+
ON legacy_report_output_id_map (canonical_report_output_id);

policyengine_api/data/initialise_local.sql

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ DROP TABLE IF EXISTS user_policies;
88
DROP TABLE IF EXISTS tracers;
99
DROP TABLE IF EXISTS report_output_runs;
1010
DROP TABLE IF EXISTS simulation_runs;
11-
DROP TABLE IF EXISTS legacy_report_output_aliases;
11+
DROP TABLE IF EXISTS legacy_report_output_id_map;
1212

1313
CREATE TABLE IF NOT EXISTS household (
1414
id INTEGER PRIMARY KEY,
@@ -153,6 +153,11 @@ CREATE TABLE IF NOT EXISTS report_outputs (
153153
latest_successful_run_id CHAR(36) DEFAULT NULL
154154
);
155155

156+
CREATE INDEX report_outputs_identity_idx
157+
ON report_outputs (
158+
country_id, report_identity_hash, report_identity_schema_version
159+
);
160+
156161
CREATE TABLE IF NOT EXISTS report_output_runs (
157162
id CHAR(36) PRIMARY KEY,
158163
report_output_id INT NOT NULL,
@@ -201,7 +206,13 @@ CREATE TABLE IF NOT EXISTS simulation_runs (
201206
UNIQUE (simulation_id, run_sequence)
202207
);
203208

204-
CREATE TABLE IF NOT EXISTS legacy_report_output_aliases (
209+
CREATE INDEX simulation_runs_report_output_run_idx
210+
ON simulation_runs (report_output_run_id);
211+
212+
CREATE TABLE IF NOT EXISTS legacy_report_output_id_map (
205213
legacy_report_output_id INT PRIMARY KEY,
206214
canonical_report_output_id INT NOT NULL
207215
);
216+
217+
CREATE INDEX legacy_report_output_id_map_canonical_idx
218+
ON legacy_report_output_id_map (canonical_report_output_id);

policyengine_api/routes/report_output_routes.py

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,53 @@ def get_report_output(country_id: str, report_id: int) -> Response:
195195
)
196196

197197

198+
@report_output_bp.route("/<country_id>/report/<int:report_id>/rerun", methods=["POST"])
199+
@validate_country
200+
def create_report_rerun(country_id: str, report_id: int) -> Response:
201+
"""
202+
Create a new pending run for an existing report.
203+
204+
The requested report ID may be a legacy ID; the run is always created under
205+
the resolved canonical report output.
206+
"""
207+
payload = request.json or {}
208+
if not isinstance(payload, dict):
209+
raise BadRequest("Payload must be an object")
210+
211+
version_manifest_overrides = _parse_report_run_metadata(payload)
212+
213+
try:
214+
if not report_output_service.report_output_exists(country_id, report_id):
215+
raise NotFound(f"Report #{report_id} not found.")
216+
217+
rerun = report_output_service.create_report_rerun(
218+
country_id=country_id,
219+
report_output_id=report_id,
220+
version_manifest_overrides=version_manifest_overrides,
221+
)
222+
except HTTPException:
223+
raise
224+
except ValueError as e:
225+
current_app.logger.warning(
226+
"Bad request creating report rerun #%s for country %s: %s",
227+
report_id,
228+
country_id,
229+
e,
230+
)
231+
raise BadRequest(f"Failed to create report rerun: {e}") from e
232+
233+
response_body = dict(
234+
status="ok",
235+
message="Report rerun created successfully",
236+
result=rerun,
237+
)
238+
return Response(
239+
json.dumps(response_body),
240+
status=201,
241+
mimetype="application/json",
242+
)
243+
244+
198245
@report_output_bp.route("/<country_id>/report", methods=["PATCH"])
199246
@validate_country
200247
def update_report_output(country_id: str) -> Response:
@@ -206,6 +253,7 @@ def update_report_output(country_id: str) -> Response:
206253
207254
Request body can contain:
208255
- id (int): The report output ID.
256+
- report_output_run_id (str | None): Specific report run to update.
209257
- status (str): The new status ('pending', 'running', 'complete', or 'error')
210258
- output (dict): The result output (for complete status)
211259
- api_version (str): The API version of the report
@@ -221,6 +269,7 @@ def update_report_output(country_id: str) -> Response:
221269
report_id = payload.get("id")
222270
output = payload.get("output")
223271
error_message = payload.get("error_message")
272+
report_output_run_id = payload.get("report_output_run_id")
224273
version_manifest_overrides = _parse_report_run_metadata(payload)
225274
print(f"Updating report #{report_id} for country {country_id}")
226275

@@ -236,6 +285,8 @@ def update_report_output(country_id: str) -> Response:
236285
# Validate that complete status has output
237286
if status == "complete" and output is None:
238287
raise BadRequest("output is required when status is 'complete'")
288+
if report_output_run_id is not None and not isinstance(report_output_run_id, str):
289+
raise BadRequest("report_output_run_id must be a string")
239290

240291
try:
241292
# First check if the report output exists without running pointer sync:
@@ -251,17 +302,14 @@ def update_report_output(country_id: str) -> Response:
251302
status=status,
252303
output=output,
253304
error_message=error_message,
305+
report_output_run_id=report_output_run_id,
254306
version_manifest_overrides=version_manifest_overrides,
255307
)
256308

257309
if not success:
258310
raise BadRequest("No fields to update")
259311

260-
# Get the updated stored record so stale-runtime jobs do not appear to
261-
# complete the current runtime lineage in the PATCH response.
262-
updated_report = report_output_service.get_stored_report_output(
263-
country_id, report_id
264-
)
312+
updated_report = report_output_service.get_report_output(country_id, report_id)
265313

266314
response_body = dict(
267315
status="ok",

policyengine_api/routes/simulation_routes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ def update_simulation(country_id: str) -> Response:
178178
179179
Request body can contain:
180180
- id (int): The simulation ID.
181+
- simulation_run_id (str | None): Specific simulation run to update.
181182
- status (str): The new status ('complete' or 'error')
182183
- output (dict): The result output (for complete status)
183184
- api_version (str): The API version of the simulation
@@ -193,6 +194,7 @@ def update_simulation(country_id: str) -> Response:
193194
simulation_id = payload.get("id")
194195
output = payload.get("output")
195196
error_message = payload.get("error_message")
197+
simulation_run_id = payload.get("simulation_run_id")
196198
version_manifest_overrides = _parse_simulation_run_metadata(payload)
197199
print(f"Updating simulation #{simulation_id} for country {country_id}")
198200

@@ -203,6 +205,8 @@ def update_simulation(country_id: str) -> Response:
203205
# Validate that complete status has output
204206
if status == "complete" and output is None:
205207
raise BadRequest("output is required when status is 'complete'")
208+
if simulation_run_id is not None and not isinstance(simulation_run_id, str):
209+
raise BadRequest("simulation_run_id must be a string")
206210

207211
try:
208212
# First check if the simulation exists
@@ -219,6 +223,7 @@ def update_simulation(country_id: str) -> Response:
219223
status=status,
220224
output=output,
221225
error_message=error_message,
226+
simulation_run_id=simulation_run_id,
222227
version_manifest_overrides=version_manifest_overrides,
223228
)
224229

policyengine_api/services/report_output_alias_service.py

Lines changed: 0 additions & 132 deletions
This file was deleted.

0 commit comments

Comments
 (0)