Deletion API
scinr.newton.ingest.deletion.delete_document
async
delete_document(
path: str | None = None,
version: int | None = None,
*,
tenant_id: str | None,
created_by_user_id: str | Sequence[str] | None = None,
job_id: str | Sequence[str] | None = None,
) -> DeletionResult
Completely delete Document node(s), their entire cascade, and orphans.
Unlike delete_document_content() (which only wipes content for
in-place re-ingestion and keeps the :Document node), this permanently
removes every :Document node matching the selector below along with:
- Every descendant reached via
IS_COMPOSED_OF*downwards from the matched nodes: deleting a folder deletes its whole subtree, while deleting a leaf by path leaves its parent folder and siblings untouched (they only go too when they match the selector themselves, e.g. the same job_id). - All :StructureNode descendants (
HAS_STRUCTURE/HAS_CHILD), their :InfoUnit, :ModelDecision, :ProposedModel, :ProposedField, and :ExtractionResult children.
Target selection
tenant_id is mandatory (keyword-only, no default — omitting it is a
TypeError) and always applied: the deletion never leaves that
tenant's documents. tenant_id=None explicitly means "public
documents" (stored as "__public__", and tenant_id="__public__"
means the same); there is no way to delete across tenants. Because every tenant has its own folder documents, the
IS_COMPOSED_OF* cascade below stays within the tenant too.
On top of the tenant, exactly one of path or job_id must be provided
(ValueError otherwise):
- path: delete the tenant's Document at that
path(all versions, or only version when given). - job_id: delete every Document of the tenant whose
job_idproperty is any of these values (onestror several) — across all paths and versions of those ingestion runs.
created_by_user_id (one str or several, matched with IN), when
given, is an additional AND filter applied on top of either selector. Left as None it means "do not filter on this
property" (it does not mean "the property must be null"). version
is also accepted as an extra filter in job_id mode.
Who may delete a public document is an authorization decision for the calling API layer; this function only enforces the scope.
Before any Neo4j deletion happens, this also deletes the documental
storage records (raw binary + converted Markdown pages) for every
non-empty raw_file_id found on the target Document(s) and their
IS_COMPOSED_OF* descendants, via the configured storage backend
(storage/factory.py::get_storage()). This step is fail-fast: if
deleting storage for any raw_file_id raises an unexpected exception,
it propagates immediately and the Neo4j cascade delete is never run.
The cascade runs in bounded transactions (ingest/_cascade.py), not
in a single one: one transaction holding the subtree of hundreds of
documents overruns the server's transaction memory. It is therefore not
atomic — the :Document nodes are marked deletion_pending first and
deleted last, so if it fails half-way, calling delete_document()
again with the same selector deletes what is left. It also removes the
:ComplementaryMatch and :SupplementaryField nodes of the annotations.
Before the :Document nodes go, the :Entity/:ModelInstance and
:LabeledEntity nodes of the same tenant this deletion leaves orphaned
are deleted too (ingest/_gc.py). Only what hung from the deleted
:ExtractionResult nodes is checked, so the cost follows the size of the
deletion, not of the tenant; orphans left by anything else stay until
:func:collect_orphans is run. The exception is a call that finishes an
interrupted delete or freeze: the candidates of the interrupted run are
lost, so it sweeps the whole tenant (up to :data:GC_MAX_PASSES
iterations per pass), as collect_orphans() does.
Opens and closes its own Neo4j driver — does not require the caller to
manage one. The Neo4j-specific work (existence check, raw_file_id
lookup, cascade delete, GC passes) uses the existing synchronous Neo4j
driver under the hood, dispatched via asyncio.to_thread(); the
storage deletion calls are awaited directly since storage repositories
(Motor-backed) are natively async.
Returns
DeletionResult
Structured counts of everything deleted. If no Document matches the
selector, found is False and all counters are 0 (no storage,
delete, or GC queries are executed in that case).
Raises
TypeError If tenant_id is not passed. ValueError If neither or both of path and job_id are provided, or tenant_id is empty, or job_id / created_by_user_id is an empty list.
scinr.newton.ingest.deletion.collect_orphans
async
Delete every orphaned :Entity, :ModelInstance and :LabeledEntity node of one tenant.
delete_document() and freeze_document() only collect the orphans
they cause. This maintenance call collects the rest: the nodes left
behind by the write paths that delete :ExtractionResult nodes without
collecting (re-ingestion with update_mode=True, re-extraction of a
node), and by an interrupted delete or freeze that was never run again.
An :Entity / :ModelInstance is an orphan when no :ExtractionResult reaches it within 7 hops; a :LabeledEntity, when nothing points at it. Every such node of the tenant is checked, so the cost grows with the tenant, not with what there is to collect: run it from time to time, not after every operation.
Do not run it while the same tenant is being ingested: a node written just before the :ExtractionResult that will point at it is, for that instant, an orphan.
tenant_id is mandatory (keyword-only, no default); None and
"__public__" both mean the public documents. Opens and closes its own
Neo4j driver.
Returns
OrphanCollectionResult How many nodes were deleted, and in how many iterations.
Raises
TypeError If tenant_id is not passed. ValueError If tenant_id is empty.