Add read-only support for Visual FoxPro compound index (.cdx) files - #285
Conversation
Adds a DbfDataReader.Cdx namespace that opens compound index files, enumerates the named tag indexes they contain, walks entries in key order, and searches for keys - returning entries whose RecordIndex plugs directly into DbfTable.Seek / DbfDataReader.Seek to fetch the matching rows. The format handling is ported from the dev/async fork by Dai Rees (https://github.com/daiplusplus/DbfDataReader/tree/dev/async, MIT), which reverse-engineered the hard parts: the compound tag directory, interior nodes (whose entries are big-endian record number + child pointer pairs, not the documented IDX layout), and the bit-packed, prefix- and suffix-compressed leaf key format, including the 64-bit shift promotion fix and undocumented option/attribute flags observed in real files. The exhaustive packed-entry unit tests are ported from the fork as well. This port rewrites the I/O onto the library's span/BinaryPrimitives parsing style (no BinaryReader, no Windows-only APIs), keeps node types internal behind a small public surface (CdxFile, CdxIndex, CdxKeyEntry, CdxIndexHeader), makes format validation always-on, guards sibling-chain walks against cycles, and fixes a comparer issue where a stored key that is a strict prefix of the target compared as equal (trimmed trailing bytes now compare as padding). Searching supports exact keys (string or byte[]) and a comparison delegate for range scans. Limitations, documented in the README: ascending MACHINE-collation character keys only, key expressions are exposed but not evaluated, and entries include deleted records. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Restructure the leaf key prefix copy so the previousKey null guard dominates the dereference (S2259; the guard was previously hidden from analysis behind Math.Min). - Extract interior-node descent from SearchCore into FindFirstCandidateLeaf and use FirstOrDefault for the entry scan, reducing cognitive complexity (S3776, S3267). - Parse the common node fields inside the node type readers instead of passing them individually (S107). - Rename the zero-valued CdxNodeAttributes member to None (S2346). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
@daiplusplus — eight years on, your This PR ports your CDX compound-index work upstream (credit in the PR description and commit message): the compound tag directory, the big-endian interior node entries — where you spotted that Microsoft's docs describe the wrong layout — and the bit-packed, prefix/suffix-compressed leaf keys, including your 64-bit shift promotion fix. Your exhaustive packed-entry test vectors passed unchanged against the port on the first run, and searches now feed a Thanks for the reverse-engineering effort — it held up remarkably well after all this time. If you ever feel like giving the port a once-over, I'd welcome your eyes on it. 🤖 Generated with Claude Code |
|
I'm flattered (honestly!). Regrettably I don't think I can provide any real feedback as I barely remember writing this; and re-reading through it now today gives me a slight feeling of embarrassment from patterns I've since moved-past from - and the extra rigour needed in the async codepath (e.g. I didn't carry I have some general recommendations I stick-to in all projects today though (in addition to the things I mentioned above)...
...stuff like that. Not that anyone needs to be told any of this, but you did ask ;) |
|
@chrisrichards May I ask how much of your posts are “you” as opposed to Claude acting autonomously through your account? |
|
@daiplusplus I'm not using Claude autonomously, I'm still reviewing everything myself. For the post above I asked Claude to create a draft as I genuinely wanted to thank you (especially for the work on supporting indexes), I reviewed, made some minor updates and posted it. Hope that's OK |
In these kinds of human-to-human messages I don't think it's a good idea to leave the "Generated with Claude Code" footer because it gives the impression that Claude wrote the entire comment by itself (and on its own volition) - which I imagine many people will interpret the opposite way as intended (i.e.: if you want to thank someone personally, don't ask AI to do it for you - because that's how it comes across, unfortunately). |
|
@daiplusplus Yep, point taken. Will remember for future posts |



Summary
Adds a
DbfDataReader.Cdxnamespace that opens Visual FoxPro compound index files, enumerates their named tag indexes, walks entries in key order, and searches for keys. Search results carry the DBF record number, andCdxKeyEntry.RecordIndexconverts it to the zero-based index thatDbfTable.Seek/DbfDataReader.Seek(#283) expect — so an index hit becomes a row in two lines:Credit
The CDX format handling is ported from the
dev/asyncfork by Dai Rees (@daiplusplus) (MIT), who reverse-engineered the hard parts of this format in 2017–2018:[record number][duplicate count][trailing count], with keys reconstructed via prefix compression (against the previous key) and suffix compression (trailing padding trimmed), and new key bytes growing backwards from the end of the 488-byte area;<<Int64 promotion fix for packed entries wider than 32 bits, and undocumented option/attribute flag values observed in real-world files.The exhaustive packed-entry unit test vectors are ported directly from the fork.
What the port changes relative to the fork
ReadOnlySpan<byte>+BinaryPrimitives— noBinaryReader, no Windows-onlyFileStreamoverloads, no reflection. Works on both target frameworks and all platforms.CdxFile,CdxIndex,CdxKeyEntry,CdxIndexHeader, enums,CdxException); node types and traversal internals areinternal."ABC"could also match"AB"). Trimmed bytes now compare as the pad byte (0x20), which also preserves correct B-tree ordering during descent.CurrentEncoding);Search(string)pads keys to the index key length.Limitations (documented in the README)
Ascending, byte-wise (MACHINE collation) character keys only: descending indexes throw
NotSupportedExceptionon search, FoxPro's binary key transforms for numeric/date index keys are not decoded, key/FOR expressions are exposed as strings but not evaluated, and index entries include deleted records (checkIsDeletedafter seeking)..idxsingle-index files are not supported.Test plan
44 new tests, all green (full suite: 163 passed, 1 pre-existing WIP skip):
CdxKeyPackingTests— the fork's exhaustive packed-entry bit-unpacking vectors (lengths 0–8 at multiple offsets).CdxKeyComparerTests— full-length comparison plus the trimmed-prefix regression cases.CdxTests— integration over thetest/fixtures/foxprodbfiles that have shipped (unused) in this repo since 2017:calls.CDX,contacts.CDX,setup.CDX,types.CDX, andFOXPRO-DB-TEST.DCX(database container indexes parse with the same reader). Per tag: entry count matchesCount(), every adjacent entry pair is in ascending padded-key order, exact-key searches return the correct duplicate sets, missing-key probes return empty (regression for the fork's historical infinite-loop bug), and — end to end — index entries for every tag whose key expression is a plain character column are resolved viaSeekto actual DBF rows whose column value equals the entry key.🤖 Generated with Claude Code