Skip to content

fix(core): the schema-less API reads documents a typed collection stored in overflow pages (#301) - #319

Open
CaffeinatedCoder wants to merge 2 commits into
EntglDb:mainfrom
CaffeinatedCoder:fix/301-dynamic-overflow-read
Open

CaffeinatedCoder wants to merge 2 commits into
EntglDb:mainfrom
CaffeinatedCoder:fix/301-dynamic-overflow-read

Conversation

@CaffeinatedCoder

@CaffeinatedCoder CaffeinatedCoder commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

What was wrong

A typed DocumentCollection stores a document larger than one page (about 16 KB with the default page size) across overflow pages. The schema-less API could not read such a collection: DynamicCollection.FindAllAsync threw NotSupportedException: Skipping type 69 not supported at position 7 at the first large document, and so did everything built on it: FindByIdAsync, ScanAsync, BLQL, BLite Studio's collection view, and BLiteMigration.ToMultiFileAsync / ToSingleFileAsync, which copy every collection through the schema-less API (#301). The migration also left the empty multi-file layout (.idx, wal/, collections/) next to the source.

Mechanism

A slot flagged SlotFlags.HasOverflow holds [TotalLength (4)] [FirstOverflowPage (4)] [first chunk], and each overflow page holds PageSize - SlottedPageHeader.Size bytes after its header and names the next page in NextOverflowPage (DocumentCollection.InsertWithOverflow). DynamicCollection never checked the flag and handed the slot bytes to BsonSpanReader as a document; byte 4 of the payload is a page id, not a BSON type.

Fix

  • Storage/OverflowChainReader.cs (new, internal): reassembles the document from that layout, sync (Read) and async (ReadAsync, copying what it needs from the page buffer before the first await), returning null rather than a partial document when a chain is inconsistent; ChainPages lists the chain's page ids and stops at a page that is not an overflow page. It is the same loop DocumentCollection has in three places (ReadRawBytesAt, ReadDocumentAt, its async twin); those are left as they are in this PR, so the typed hot path does not change.
  • DynamicCollection: ReadDocumentAt, ReadDocumentAtAsync and ScanAsync (whose predicate now sees the whole document) use the reader. Every public read path goes through these three (FindAllAsync, FindByIdAsync, CountAsync, QueryIndexAsync, BLQL, the LINQ provider). DeleteSlot frees the chain with FreePageOnCommit, as DocumentCollection.FreeOverflowChain does, so a schema-less delete, or an update that relocates the document (TryUpdateInPlace already refuses a flagged slot), leaks no pages.
  • Writes: DynamicCollection still does not write overflow pages. The limit is now public as DynamicCollection.MaxDocumentSize (page size minus header, slot and reserve), the rejection message names it, and the README's schema-less section says so. The write side is DynamicCollection cannot write a document larger than one page: no overflow on the schema-less write path #320; Moving from LiteDB: a guide, an API mapping and an import tool #297 (LiteDB import) will need it.
  • BLiteMigration: before inserting a document into the target DynamicCollection, a document larger than MaxDocumentSize is refused with a NotSupportedException that names the collection, _id and size and says the source is unchanged, instead of a BLiteValidationException from inside InsertBulkAsync. On failure ToMultiFileAsync now also removes the companion files it created (.idx, the WAL directory, the collection directory; ones that existed before the call are kept), and ToSingleFileAsync removes a target file it created at another path.

No format change. Overflow pages are read exactly as the typed API reads them; nothing is written differently.

Tests

DynamicOverflowReadTests, 9 tests. Each writes a small user and one with a 40,000-character name through TestDbContext, then uses BLiteEngine:

Test upstream/main this PR
FindAllAsync returns both, the large one whole; CountAsync is 2 NotSupportedException: Skipping type 171 passes
FindByIdAsync of the large one same exception passes
BLQL Eq, OrderByDescending, and Gte through a new index on age same exception passes
ScanAsync predicate sees all 40 KB empty result passes
Typed delete of the large document is seen by the schema-less API passes passes
Schema-less delete is seen by the typed API; a new 40 KB document then fits in the freed pages (file grows by less than 40 KB) fails: cannot delete passes
Schema-less update replaces the large document for both APIs passes (the chain leaked) passes
Schema-less insert over MaxDocumentSize is rejected naming the limit does not compile (new API) passes
ToMultiFileAsync refuses by collection and _id; source intact for both APIs; no .migrating, .idx, WAL or collection directory left fails: the message is the BSON reader's, and .idx / wal/ / collections/ remain passes

Full suite dotnet test tests/BLite.Tests (macOS, net10.0): 3101 passed, 0 failed, 4 skipped. BLite.Core also builds for netstandard2.1.

Compatibility

Reads that threw now return the document. DynamicCollection.MaxDocumentSize is a new public property. The two rejection messages for an oversized write changed wording ("exceeds the N bytes a DynamicCollection document may occupy (one page)"); the exception type is unchanged. BLiteMigration throws NotSupportedException for an oversized document where it threw NotSupportedException from the BSON reader before. netstandard2.1 and AOT are unaffected.

Fixes #301

🤖 Generated with Claude Code

…red in overflow pages (EntglDb#301)

A typed DocumentCollection stores a document larger than one page across
overflow pages: the slot holds [length][first overflow page][first chunk].
DynamicCollection never checked SlotFlags.HasOverflow and handed the slot
bytes to BsonSpanReader, which threw NotSupportedException "Skipping type
… not supported at position 7". FindAllAsync, FindByIdAsync, ScanAsync,
BLQL, BLite Studio and BLiteMigration all failed on the first large document.

New internal OverflowChainReader (Storage/) reassembles such a document, sync
and async, from the layout DocumentCollection writes, and lists the chain's
pages. DynamicCollection uses it in ReadDocumentAt, ReadDocumentAtAsync and
ScanAsync, and DeleteSlot frees the chain on commit (FreePageOnCommit, as
DocumentCollection.FreeOverflowChain does) so a schema-less delete or
relocating update of such a document leaks no pages. DynamicCollection still
does not write overflow pages; the limit is now public as MaxDocumentSize and
the rejection message names it.

BLiteMigration copies into a DynamicCollection, so it refuses a document
larger than MaxDocumentSize with a NotSupportedException naming the
collection, _id and size, before writing it; on failure ToMultiFileAsync also
removes the companion files (.idx, WAL directory, collection directory) it
created, and ToSingleFileAsync removes a target it created. README notes the
schema-less size limit.

Tests: DynamicOverflowReadTests, 9 tests; 6 of the 8 that compile on
upstream/main fail there (3 with the issue's exception).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@mrdevrobot mrdevrobot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested. Verified: the schema-less reads of overflow documents fail on 0450b97 and pass here. The PR adds an unrequested public DynamicCollection.MaxDocumentSize; make it internal (the tests see internals through InternalsVisibleTo) and drop the property name from the README note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CaffeinatedCoder

Copy link
Copy Markdown
Collaborator Author

Done in df2e66c: DynamicCollection.MaxDocumentSize is now internal (the tests read it through InternalsVisibleTo), and the README note says "about 16 KB of BSON with the default page size" without the property name.

This branch was successfully deployed

1 active deployment
Production — df2e66cc Deployed Oct 19, 2026 by CaffeinatedCoder via build_and_publish #797
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working package/core Affects BLite.Core

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The schema-less API cannot read a typed collection that holds a document larger than one page: FindAllAsync and BLiteMigration throw "Skipping type 69"

2 participants