Skip to content

fix(cassandra): page the delete-path scans so collections delete fully (#1066) - #1077

Open
Souptik96 wants to merge 1 commit into
trustgraph-ai:masterfrom
Souptik96:fix/1066-paginate-delete-scans
Open

fix(cassandra): page the delete-path scans so collections delete fully (#1066)#1077
Souptik96 wants to merge 1 commit into
trustgraph-ai:masterfrom
Souptik96:fix/1066-paginate-delete-scans

Conversation

@Souptik96

Copy link
Copy Markdown

Fixes #1066.

Summary

async_execute materialises only the first result page. Three delete paths used it to enumerate everything they were about to remove, so a collection larger than fetch_size (5000) was only partly deleted while the operation logged success:

File Function
trustgraph-flow/trustgraph/direct/cassandra_kg.py async_delete_collection ← reported in #1066
trustgraph-flow/trustgraph/storage/rows/cassandra/write.py delete_collection
trustgraph-flow/trustgraph/storage/rows/cassandra/write.py delete_collection_schema

All three now use async_scan, which iterates every page and discards each page as it goes, so peak memory stays bounded by fetch_size rather than by the size of the collection.

Why async_scan and not async_execute_paged (the issue suggested the latter): async_scan returns a flat list, so it is a drop-in for async_execute and none of the iteration code below the call sites had to change. async_execute_paged returns a list of pages, which would need flattening at each site and holds every page at once. Both were already present and used elsewhere in the tree; neither is new here.

The two extra sites

@zsoltboko's report ends with "Worth auditing the other async_execute callers for the same unbounded-scan pattern", so I did. The two row-storage sites are the same defect, and they are arguably worse than the reported one: both delete the row_partitions manifest rows for the collection after the truncated discovery, so the surviving rows do not merely persist — they lose the index that would let anything find them again.

I swept all 87 await async_execute( call sites in the repo, resolving multi-line and variable-held SQL. Exactly four are unbounded SELECTs, three of which are the delete paths above. The fourth is tables/config.py:116 get_version, which is SELECT version FROM version WHERE id = … — a single-row point lookup, correctly left alone. Every other caller carries an explicit LIMIT, is a point lookup, or is a write. So this should be the complete set.

Test plan

Two new test modules, 9 tests, placed next to their subjects:

  • tests/unit/test_direct/test_entity_centric_delete_pagination.py (4)
  • tests/unit/test_storage/test_rows_cassandra_delete_pagination.py (5)

The harness models both halves of the driver contract instead of stubbing the helpers out, which is what makes the tests discriminating:

session.execute(stmt, params)     -> multi-page ResultSet   (what async_scan uses)
session.execute_async(q, params)  -> first page only, via callbacks
                                     (what async_execute uses, mirroring the driver)

No production function is patched, so reverting the fix genuinely fails these tests. The storage tests bind the real methods onto a MagicMock processor, following the existing style in tests/unit/test_storage/test_rows_cassandra_storage.py, which avoids standing up the whole flow-processor framework.

Negative controls — revert one source file, keep the tests:

Reverted Result
cassandra_kg.py only 2 failed / 2 passed
write.py only 4 failed / 1 passed

In both cases the failures are exactly the paging assertions, while the guard tests pass unpatched — ..._single_page_unchanged (a collection that fits in one page must behave as before) and test_sync_delete_collection_spans_pages (the sync path was already correct and stays pinned). That is the point of including them: they assert preserved behaviour, so they must not move.

Full unit suite (pytest tests/unit, Python 3.13.4):

Result
master @ 27e0ab8 1 failed, 3090 passed, 51 skipped
this branch 1 failed, 3099 passed, 51 skipped

+9 passed is exactly the tests added. The one failure is identical on both sides and unrelated: tests/unit/test_cli/test_load_structured_data.py::TestLoadStructuredDataUnit::test_invalid_descriptor_format.

Limitations, stated plainly

  • Not reproduced against live Cassandra. I have no Docker on this machine, so I verified at the driver boundary with the paging harness above rather than against a real multi-page result set. The reporter's cqlsh counts are the end-to-end evidence; a confirming run against a >5000-quad collection would be worth doing before release.
  • tests/unit/test_decoding was excluded from both runs (3 collection errors, identical before and after). It needs trustgraph-docling, which I did not install because it pulls a torch stack. Nothing in this change is near the decoding path.
  • I installed trustgraph-base, -flow, -cli, -vertexai, -bedrock, -mcp from source with plain pip install . as pull-request.yaml does, generating the *_version.py files with VERSION=2.8.999. make is unavailable on Windows so I replicated that Makefile target directly; no generated version file is in the diff.
  • Base branch is master, matching the most recent external merges (Sanitize workspace name used as Cassandra keyspace (#1043) #1053, Update section title in README.md #1051, Update section title in README.md #1044). Happy to retarget at a release branch if that is preferred for a data-loss fix.

I will sign the CLA when the bot prompts.

async_execute materialises only the first result page. Three delete paths
used it to enumerate everything they were about to remove, so a collection
larger than fetch_size (5000) was only partly deleted while the operation
reported success:

  direct/cassandra_kg.py            async_delete_collection
  storage/rows/cassandra/write.py   delete_collection
  storage/rows/cassandra/write.py   delete_collection_schema

All three now use async_scan, which iterates every page and discards each
one as it goes, so peak memory stays bounded by fetch_size. It returns a
flat list, so no iteration code below the call had to change.

The two row-storage sites are the same defect as the reported one, found
by auditing the other async_execute callers as the issue suggests. They
are arguably worse: both delete the row_partitions manifest rows for the
collection afterwards, so a truncated discovery does not merely leave rows
behind, it removes the index that would let anything find them again.

A repo-wide sweep found no other unbounded SELECT going through
async_execute; every remaining caller either carries a LIMIT, is a point
lookup, or is a write.

Fixes trustgraph-ai#1066
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

Contributor License Agreement ✅

All contributors have signed the CLA. Thank you!

@Souptik96

Souptik96 commented Jul 31, 2026

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA.

@Souptik96

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

delete_collection silently truncates at fetch_size — leaves most data behind (Cassandra triple store)

1 participant