|
6 | 6 | import com.dbaagent.service.CredentialService; |
7 | 7 | import com.dbaagent.service.QueryExecutionContext; |
8 | 8 | import com.dbaagent.service.QueryExecutionPolicyException; |
| 9 | +import com.dbaagent.service.ActiveQueryService; |
9 | 10 | import com.dbaagent.service.QueryExecutorService; |
| 11 | +import com.dbaagent.service.RunningQueryRegistry; |
10 | 12 | import com.dbaagent.service.SqlExecutionAuditService; |
11 | 13 | import com.dbaagent.service.UserDataAccessPolicyException; |
12 | 14 | import com.dbaagent.service.UserDataAccessPolicyService; |
@@ -38,6 +40,8 @@ public class SchemaController { |
38 | 40 | private final AccessControlService accessControlService; |
39 | 41 | private final SqlExecutionAuditService sqlExecutionAuditService; |
40 | 42 | private final UserDataAccessPolicyService userDataAccessPolicyService; |
| 43 | + private final RunningQueryRegistry runningQueryRegistry; |
| 44 | + private final ActiveQueryService activeQueryService; |
41 | 45 |
|
42 | 46 | @PostMapping("/scan") |
43 | 47 | public ResponseEntity<Map<String, Object>> scanSchema(@PathVariable String connectionId) { |
@@ -265,6 +269,63 @@ public ResponseEntity<Map<String, Object>> executeQuery( |
265 | 269 | } |
266 | 270 | } |
267 | 271 |
|
| 272 | + /** |
| 273 | + * Terminates a query this caller started but abandoned. Aborting the HTTP |
| 274 | + * request only closes the socket — the statement keeps running and holds a |
| 275 | + * pooled connection until it finishes, so the client must ask for it to stop. |
| 276 | + */ |
| 277 | + @PostMapping("/query/{executionId}/cancel") |
| 278 | + public ResponseEntity<Map<String, Object>> cancelQuery( |
| 279 | + @PathVariable String connectionId, |
| 280 | + @PathVariable String executionId) { |
| 281 | + Map<String, Object> response = new HashMap<>(); |
| 282 | + try { |
| 283 | + if (!credentialService.connectionExists(connectionId)) { |
| 284 | + response.put("success", false); |
| 285 | + response.put("message", "Connection not found"); |
| 286 | + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); |
| 287 | + } |
| 288 | + accessControlService.assertCanUseChatEditor(connectionId); |
| 289 | + |
| 290 | + var running = runningQueryRegistry.find(executionId); |
| 291 | + if (running.isEmpty()) { |
| 292 | + // Already finished, or never started. Nothing to cancel. |
| 293 | + response.put("success", true); |
| 294 | + response.put("cancelled", false); |
| 295 | + response.put("message", "Query is no longer running"); |
| 296 | + return ResponseEntity.ok(response); |
| 297 | + } |
| 298 | + |
| 299 | + RunningQueryRegistry.RunningQuery target = running.get(); |
| 300 | + // The execution id is a bearer token for a kill: scope it to this |
| 301 | + // connection and to the user who started it, so one caller cannot |
| 302 | + // terminate another's query by guessing an id. |
| 303 | + String currentUser = accessControlService.getCurrentUsername(); |
| 304 | + if (!connectionId.equals(target.connectionId()) |
| 305 | + || (target.username() != null && currentUser != null && !target.username().equals(currentUser))) { |
| 306 | + response.put("success", false); |
| 307 | + response.put("message", "Query not found for this connection"); |
| 308 | + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); |
| 309 | + } |
| 310 | + |
| 311 | + activeQueryService.killQuery(connectionId, target.sessionPid()); |
| 312 | + runningQueryRegistry.unregister(executionId); |
| 313 | + response.put("success", true); |
| 314 | + response.put("cancelled", true); |
| 315 | + response.put("message", "Query cancelled"); |
| 316 | + return ResponseEntity.ok(response); |
| 317 | + } catch (ResponseStatusException e) { |
| 318 | + response.put("success", false); |
| 319 | + response.put("message", e.getReason()); |
| 320 | + return ResponseEntity.status(e.getStatusCode()).body(response); |
| 321 | + } catch (Exception e) { |
| 322 | + log.warn("Failed to cancel query {} on connection {}: {}", executionId, connectionId, e.getMessage()); |
| 323 | + response.put("success", false); |
| 324 | + response.put("message", "Failed to cancel query: " + e.getMessage()); |
| 325 | + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); |
| 326 | + } |
| 327 | + } |
| 328 | + |
268 | 329 | // `{tableName:.+}` keeps schema-qualified ids (`crm.orders`) as one segment. |
269 | 330 | @GetMapping("/tables/{tableName:.+}/indexes") |
270 | 331 | public ResponseEntity<Map<String, Object>> getTableIndexes( |
|
0 commit comments