fix: NTX Seek descent + SET DELETED seek + BOF — 82/82 stress test PASS
NTX Seek (ntx.go): - Always descend to leaf even on internal match (Harbour behavior) Prevents SEEK returning internal separator instead of first leaf entry Fixes duplicate key SEEK (NYC=9→10, Paris=8→10) - fStop flag tracks path match, verified at leaf with key comparison - Handle fStop at page end: ascend via nextKey to find actual match SET DELETED + SEEK (indexer.go): - When SEEK finds a deleted record with SET DELETED ON: Skip forward through matching deleted records If all matching records deleted → return not found (EOF) Fixes H04: deleted record now correctly returns .F. BOF (indexer.go + dbf.go): - Set a.FBof AFTER a.GoTo returns (GoTo resets FBof=false at line 393) - Fixes infinite loop in DO WHILE !BOF() ... SKIP -1 Results: - Unit tests: 14 packages ALL PASS - 77-item thorough test: 77/77 (100%) - 82-item stress test: 82/82 (100%) — Harbour identical Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -412,6 +412,38 @@ func (a *DBFArea) Seek(key hbrt.Value, softSeek bool, findLast bool) (bool, erro
|
||||
if exactFound && recNo > 0 {
|
||||
a.GoTo(recNo)
|
||||
a.FEof = false
|
||||
// SET DELETED ON: if found record is deleted, skip to next non-deleted with same key
|
||||
if hbrdd.IsSetDeleted != nil && hbrdd.IsSetDeleted() && a.Deleted() {
|
||||
// Skip forward through deleted records
|
||||
for {
|
||||
idx.SkipNext()
|
||||
if idx.IsEOF() {
|
||||
break
|
||||
}
|
||||
// Check if key still matches (partial or full)
|
||||
curKey := idx.CurKey()
|
||||
if actualLen < keyLen {
|
||||
if !bytes.Equal(curKey[:actualLen], searchKey[:actualLen]) {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if !bytes.Equal(curKey, searchKey) {
|
||||
break
|
||||
}
|
||||
}
|
||||
a.GoTo(idx.CurRecNo())
|
||||
if !a.Deleted() {
|
||||
a.SetFound(true)
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
// All matching records are deleted
|
||||
rc, _ := a.RecCount()
|
||||
a.GoTo(rc + 1)
|
||||
a.FEof = true
|
||||
a.SetFound(false)
|
||||
return false, nil
|
||||
}
|
||||
a.SetFound(true)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -244,6 +244,10 @@ func (idx *Index) Seek(searchKey []byte) (uint32, bool) {
|
||||
pageOffset := int64(idx.header.Root)
|
||||
|
||||
// Phase 1: Traverse from root to leaf
|
||||
// Harbour: always descend to leaf, even if match found in internal page.
|
||||
// This ensures SEEK finds the FIRST (lowest RecNo) occurrence of duplicate keys.
|
||||
// fStop tracks whether any page had an exact match along the path.
|
||||
fStop := false
|
||||
for {
|
||||
page, err := LoadPage(idx.file, pageOffset)
|
||||
if err != nil {
|
||||
@@ -252,6 +256,9 @@ func (idx *Index) Seek(searchKey []byte) (uint32, bool) {
|
||||
}
|
||||
|
||||
iKey, found := idx.pageKeyFind(page, searchKey, false, 0)
|
||||
if found {
|
||||
fStop = true
|
||||
}
|
||||
|
||||
// Push onto stack
|
||||
if idx.stackLevel < StackSize {
|
||||
@@ -262,24 +269,33 @@ func (idx *Index) Seek(searchKey []byte) (uint32, bool) {
|
||||
idx.stackLevel++
|
||||
}
|
||||
|
||||
if found {
|
||||
// Exact match found at this page
|
||||
idx.curRecNo = page.KeyRecNo(iKey)
|
||||
copy(idx.curKey, page.KeyValue(iKey, idx.keyLen))
|
||||
return idx.curRecNo, true
|
||||
}
|
||||
|
||||
// Follow child pointer
|
||||
// Follow child pointer (always descend, even on match)
|
||||
childOffset := page.KeyChild(iKey)
|
||||
if childOffset == 0 {
|
||||
// At leaf — no exact match
|
||||
// Position at this key (next higher) for SOFTSEEK
|
||||
// At leaf — check if the current position has a matching key
|
||||
if iKey < int(page.keyCount) {
|
||||
idx.curRecNo = page.KeyRecNo(iKey)
|
||||
copy(idx.curKey, page.KeyValue(iKey, idx.keyLen))
|
||||
// Check if this leaf key actually matches the search key
|
||||
leafMatch := (bytes.Compare(searchKey, page.KeyValue(iKey, len(searchKey))) == 0)
|
||||
if leafMatch || fStop {
|
||||
// Verify it's a real match by comparing actual key content
|
||||
if bytes.Compare(idx.curKey[:len(searchKey)], searchKey[:len(searchKey)]) == 0 {
|
||||
return idx.curRecNo, true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Past end of page — try next via stack
|
||||
if idx.nextKey() {
|
||||
if fStop {
|
||||
// We matched on an internal page but descended to wrong child
|
||||
// The match is the parent separator — go back via nextKey
|
||||
if idx.nextKey() {
|
||||
// Check if nextKey is actually a match
|
||||
if bytes.Compare(idx.curKey[:len(searchKey)], searchKey[:len(searchKey)]) == 0 {
|
||||
return idx.curRecNo, true
|
||||
}
|
||||
}
|
||||
} else if idx.nextKey() {
|
||||
return idx.curRecNo, false
|
||||
}
|
||||
idx.tagEOF = true
|
||||
|
||||
Reference in New Issue
Block a user