Known issues — M3 / M3.5
A code review on 2026-05-03 surfaced compatibility-breaking deviations
from the FoxPro CDX / Clipper NTX on-disk specs. The implementations
round-trip their own output but cannot interoperate with .cdx /
.ntx files produced by other tools, which contradicts the
README “Validation” goal of byte-level compatibility.
Status — M3.6 / M3.7
| # | Issue | Status |
|---|---|---|
| 1 | CDX leaf entry bit width hardcoded | Fixed (c1b8cf8). Encoder uses Harbour-equivalent compute_layout (bBits derived from key length). |
| 3 | CDX branch descent wrong endian / offset | Fixed (7f3041f). seek_first now reads child as BE per hb_cdxPageGetKeyPage. |
| 5 | NTX insert returns AE_FUNCTION_NOT_AVAILABLE on second page |
Fixed for single-level (6ab97c4). Root-leaf overflow now creates a branch root with two leaves. Multi-level recursion still pending. |
| 6 | AdsOpenIndex lifecycle race | Fixed (efe8d22). |
| 7 | AdsCreateIndex indexes deleted records | Fixed (efe8d22). |
| 10 | NTX erase ignores recno when recno=0 | Confirmed-as-spec. Matches Harbour’s NTX_IGNORE_REC_NUM = 0x0UL convention (passing recno=0 means “any recno with this key”). |
| 11 | NTX soft seek past end | Confirmed-as-correct. The seek loop already descends the trailing right child when i >= kc on a non-leaf path; soft fallback only trips when reaching a leaf, which lands on the last key with AfterKey. |
| 12 | Descending / unique flag round-trip untested | Fixed (bb75c22). |
| 4 | NTX multi-level next / prev correctness |
Fixed (aaf8f52, M3.8). Cache-based in-order traversal + leaf-split fix (separator promoted, not duplicated). |
| 2 | CDX compound structure tag | Fixed (M3.9). Compound layout: file header @0, structure-tag root leaf @1024, sub-tag header @1536, sub-tag data @2560+. |
All compat-breaking reviewer items now closed. Multi-tag-per-file is also supported as of M3.10 (CdxIndex::add_tag appends sub-tags to an existing compound CDX, CdxIndex::open_named selects by tag name, CdxIndex::list_tags enumerates).
- #4 (closed M3.8) was solved via a cache-based traversal:
ensure_cache_()walks the tree depth-first into a flatstd::vector<CachedKey>;seek_first/seek_last/seek_key/next/prevoperate on the vector by index. Write paths (insert/erase) keep stack-based descent viaseek_key_for_write_and markcache_dirty_ = true. A separate split-bug fix (Entry separator = all[mid]; right_half = [mid+1, end)) prevents the separator from appearing twice during multi-level walks. - #2 (closed M3.9).
CdxIndex::createnow writes the compound layout: file/structure-tag header at offset 0 withroot_page = 1024andkey_size = 10(tag-name length); structure-tag’s root leaf at offset 1024 holding one compact entry mapping the padded tag name to the sub-tag header offset (1536); sub-tag CDXTAGHEADER at 1536 with the sub-tag’s actualkey_size,key_expr, andunique/descendflags; sub-tag B+tree pages allocated from offset 2560 onwards.open()reads the file header to discover the structure-tag root, decodes its leaf to find the first sub-tag, then loads that sub-tag’s header.rewrite_header_writes the sub-tag header atsub_header_offset_, never at offset 0. As a side fix, the leaf bit-pack encoder haddupandtrlswapped; the original tests never tripped it because every key haddup = trl = 0. The structure-tag leaf (trl = 8for a 2-char tag name padded to 10) exposed it; the encoder now matches the decoder layout.
Items 8, 9, 13, 14, 15, 16, 17 are minor / hygiene and tracked below.
Critical (compat-breaking)
- CDX leaf entry bit width is hardcoded.
src/drivers/cdx/cdx_index.cpp:163-167always writesrecBits=24, dupBits=8, trlBits=8(5 bytes per entry). FoxPro derives these dynamically:bBits = ceil(log2(keylen+1)),dupBits = trlBits = bBits, totalkeyBytes ∈ {3,4,5}. Forkeylen=4real layout is 18/3/3 bits packed in 3 bytes. - CDX tag name stashed in
reserved2.src/drivers/cdx/cdx_index.cpp:117-120, 571-573writes the tag name at offset 24 ofCDXTAGHEADER, which corrupts the FoxProreserved2[68]. Real FoxPro CDX is compound: page 0 is the structure tag; sub-tag names live as keys in the structure tag’s B+tree, pointing at sub-tag header pages. Single-tag flat layout is non-standard. - CDX branch descent uses wrong offset and endianness.
src/drivers/cdx/cdx_index.cpp:310-316readsLE_UINT32(base + 12 + key_size + 4)for the leftmost child. Per Harbourdbfcdx1.c:1554-1556, the child page is stored big-endian at(iKey+1) * (key_size+8) - 4. Multi-level CDX trees are unreachable. - NTX
seek_key/next/prevsemantics are wrong on multi-level trees.src/drivers/ntx/ntx_index.cpp:248-258returnsExacton an internal-node match without descending, thennext()re-descends the right subtree → keys can be revisited. - NTX
insertreturnsAE_FUNCTION_NOT_AVAILABLEon the second page.src/drivers/ntx/ntx_index.cpp:424-426. The plan and the README “M3 Done” row claim full insert with split. AdsOpenIndexlifecycle race.src/abi/ace_exports.cpp:464-490callsTable::set_order(...)which destroys any previous order viastd::optional::emplace, leaving the prior index handle dangling inindex_bindings(). A subsequentAdsCloseIndex(stale)then clears the live order.AdsCreateIndexindexes deleted records.src/abi/ace_exports.cpp:541-549walks every recno viagoto_recordwithout checkingis_deleted(). The resulting index produces phantom recnos.
Important (incomplete coverage)
- CDX
seek_keyalways re-descends fromseek_first— O(N) over the leaf chain instead of O(log N) via root descent. - CDX
insertmutatesfile_size_only in memory; a freshly created+inserted CDX read by another process beforeflush()returns garbage. - NTX
eraseignoresrecnowhenrecno == 0but enforces it otherwise — opposite of Harbourhb_ntxPageKeyDelsemantics. - NTX soft seek past end leaves the cursor empty when
i ≥ kcon a non-leaf path; should land on the last key withAfterKey. - No tests round-trip
descending=trueorunique=trueflags through reopen; descending order is silently broken. - CDX test fixtures are all “create→insert→reopen” — zero coverage of real FoxPro byte sequences.
Minor
src/drivers/cdx/cdx_index.cpp:283silently writesfreeSpc=0when entries collide with suffix bytes; should error.- NTX
format_empty_pagedoesn’t reuse freed offset slots — divergent from Harbour but tolerable. src/engine/order.cppis the placeholder line; class is header-only — collapse into header or move bodies.AdsSetIndexDirectionreturns5004;descend_is already on the index and could trivially be flipped on a follow-up.
M3.6 plan (TBD)
A dedicated milestone will:
- Replace CDX bit-packing with the FoxPro-derived
bBitsformula (cite Harbourhb_cdxPageLeafInitSpace). - Implement the compound CDX layout: structure tag at page 0, sub-tag headers reachable via the structure tag B+tree.
- Fix CDX branch descent (BE child pointers at the correct offset).
- Land the NTX leaf split and revisit descent / next / prev for multi-level trees.
- Tighten the
AdsOpenIndex/AdsCloseIndexlifecycle so installing a new order does not orphan prior bindings. - Skip deleted records in
AdsCreateIndex. - Add tests: descending flag round-trip, unique flag round-trip, soft-seek past-end, branch descent, multi-leaf walks, and at least one fixture file produced by Harbour
dbfcdxso OpenADS proves it can read a real FoxPro.cdx.
Until M3.6 lands, the index path is functional only against indexes
created by OpenADS itself. For interoperability with applications
that already have .cdx / .ntx files on disk, defer to M3.6.