POST /api/clear — drop every node and relationship. Used by the UI's Settings → "Clear Database" button in server mode (Fix #11). Closes the live store, removes the underlying LadybugDB files, then opens a fresh empty store and swaps it back in. The atomic file-repla
(request: Request)
| 578 | return JSONResponse(dict(active[0])) |
| 579 | |
| 580 | async def clear_database(request: Request) -> JSONResponse: |
| 581 | """POST /api/clear — drop every node and relationship. |
| 582 | |
| 583 | Used by the UI's Settings → "Clear Database" button in server |
| 584 | mode (Fix #11). Closes the live store, removes the underlying |
| 585 | LadybugDB files, then opens a fresh empty store and swaps it |
| 586 | back in. The atomic file-replace is simpler and safer than |
| 587 | iterating every repo through `delete_repo()` — and matches |
| 588 | the spirit of what the user clicked ("delete everything"). |
| 589 | |
| 590 | Refuses while an `/api/index-url` job is running because that |
| 591 | worker also writes to the on-disk DB; clearing under its feet |
| 592 | would corrupt the staging swap. |
| 593 | """ |
| 594 | if db_path is None: |
| 595 | return _error(503, "Server was started without --db; clear is unavailable") |
| 596 | |
| 597 | # Atomically reserve exclusivity against index-url: check that no job |
| 598 | # is active AND register a sentinel under the SAME lock acquisition. |
| 599 | # Without this, a concurrent index-url could pass its own guard |
| 600 | # between our check and our unlink, then have its pipeline swap a |
| 601 | # staging DB over the files we're deleting (clear-vs-index TOCTOU). |
| 602 | # The index-vs-index hole is already closed by index_url's atomic |
| 603 | # check-and-insert; this extends the same guard to clear. |
| 604 | clear_job_id = uuid.uuid4().hex |
| 605 | with index_jobs_lock: |
| 606 | active = [jid for jid, st in index_jobs.items() if not st.get("done")] |
| 607 | if active: |
| 608 | return _error( |
| 609 | 409, |
| 610 | "An index job is currently running — wait for it to finish before clearing the database.", |
| 611 | ) |
| 612 | index_jobs[clear_job_id] = { |
| 613 | "jobId": clear_job_id, |
| 614 | "kind": "clear", |
| 615 | "phase": "clearing", |
| 616 | "message": "Clearing database", |
| 617 | "done": False, |
| 618 | "error": None, |
| 619 | "result": None, |
| 620 | "startedAt": time.time(), |
| 621 | } |
| 622 | |
| 623 | from pathlib import Path as _Path |
| 624 | |
| 625 | try: |
| 626 | with store_lock: |
| 627 | cur = store_ref["store"] |
| 628 | if cur is not None: |
| 629 | try: |
| 630 | cur.close() |
| 631 | except Exception: |
| 632 | logger.warning("Failed to close store cleanly before clear", exc_info=True) |
| 633 | store_ref["store"] = None |
| 634 | |
| 635 | # Drop the on-disk artifacts. Mirrors what `opentraceai |
| 636 | # index` does when it swaps staging into place — same set |
| 637 | # of files to remove. Cleared in this order so a partial |
nothing calls this directly
no test coverage detected