---
title: 'Delete domain term endpoint'
type: 'feature'
created: '2026-04-10'
status: 'done'
baseline_commit: 'b06a0fa'
context:
  - '_planning/docs/services/textiq-doc-extraction/architecture.md'
---

<frozen-after-approval reason="human-owned intent — do not modify unless human renegotiates">

## Intent

**Problem:** The domain terms API supports create (POST), bulk create (POST /bulk), and update (PATCH), but has no way to delete a term. APA needs to delete domain terms by ID (TEXTIQ-343).

**Approach:** Add `DELETE /{term_id}` to the existing `domain_terms_app` in `airflow/plugins/api/domain_terms.py`. Follow existing patterns: HMAC auth, RLS tenant isolation via `rls_connection`, 404 if term not found within tenant scope, 204 No Content on success. Cascade deletion to chunk metadata — remove the term from `domain_terms` arrays in PG (`data_hierarchical_nodes` JSONB) and Milvus, mirroring the PATCH cascade pattern.

## Boundaries & Constraints

**Always:**
- HMAC authentication + RLS tenant isolation (same as existing endpoints)
- Verify term exists within tenant's RLS scope before deletion (prevents cross-tenant deletion)
- Cascade: remove `term_original` from `domain_terms` JSONB arrays in `data_hierarchical_nodes` (PG) and Milvus chunks, using the same helpers as the PATCH cascade (`query_chunks_with_term`, `batch_upsert_chunks`)
- Return 204 No Content on successful deletion (no response body)

**Never:**
- Allow deletion without tenant ownership verification (RLS enforces this)
- Import from `src.*` (Airflow plugin restriction)
- Return the deleted record in the response body

## I/O & Edge-Case Matrix

| Scenario | Input / State | Expected Output / Behavior | Error Handling |
|----------|--------------|---------------------------|----------------|
| Happy path (no chunks) | Valid HMAC, existing term_id, no chunks reference term | 204 No Content, dictionary row deleted | N/A |
| Happy path (with chunks) | Valid HMAC, existing term_id, chunks reference term | 204 No Content, dictionary row deleted, term removed from PG + Milvus chunks | N/A |
| Term not found | Valid HMAC, non-existent term_id | N/A | 404 NOT_FOUND |
| Cross-tenant term | Valid HMAC, term_id owned by different tenant | N/A | 404 NOT_FOUND (RLS filters it) |
| No auth | Missing/invalid HMAC headers | N/A | 401 Unauthorized |

</frozen-after-approval>

## Code Map

- `airflow/plugins/api/domain_terms.py` -- Add DELETE /{term_id} endpoint
- `tests/airflow/test_plugins/test_domain_terms.py` -- Add tests for delete scenarios

## Tasks & Acceptance

**Execution:**
- [x] `airflow/plugins/api/domain_terms.py` -- Add `DELETE /{term_id}` endpoint: verify term exists via RLS SELECT, fetch `term_original`, cascade remove from PG chunks and Milvus, DELETE dictionary row, return 204
- [x] `tests/airflow/test_plugins/test_domain_terms.py` -- Add TestDeleteDomainTerm class covering all I/O matrix scenarios including cascade verification

**Acceptance Criteria:**
- Given valid HMAC auth and an existing term owned by the tenant, when DELETE /api/v1/domain-terms/{term_id}, then the term is deleted from dictionary, removed from PG chunk JSONB arrays and Milvus chunk metadata, and 204 returned
- Given valid HMAC auth and a non-existent or cross-tenant term_id, when DELETE, then 404 NOT_FOUND returned
- Given no HMAC auth, when DELETE, then 401 returned

## Verification

**Commands:**
- `python -m pytest tests/airflow/test_plugins/test_domain_terms.py -v` -- expected: all tests pass

## Suggested Review Order

- DELETE endpoint entry point — RLS lookup, cascade, then dictionary delete
  [`domain_terms.py:284`](../../airflow/plugins/api/domain_terms.py#L284)

- PG cascade — remove term from JSONB arrays using COALESCE + jsonb_agg filter
  [`domain_terms.py:303`](../../airflow/plugins/api/domain_terms.py#L303)

- Milvus cascade — query affected chunks, filter out term, batch upsert
  [`domain_terms.py:317`](../../airflow/plugins/api/domain_terms.py#L317)

- Dictionary row deletion — final step after cascades complete
  [`domain_terms.py:327`](../../airflow/plugins/api/domain_terms.py#L327)

- Tests — happy path (no chunks), cascade verification, 404, 401
  [`test_domain_terms.py:499`](../../tests/airflow/test_plugins/test_domain_terms.py#L499)
