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