fix: NTX B-tree — proper pageSplit from Harbour + BOF detection
NTX Build (build.go): - pageSplit: exact port of Harbour hb_ntxPageSplit - NewPage = LEFT half (lower keys), OldPage = RIGHT half (offset-swapped) - Proper offset table initialization for all pages - setKeyEntry/copyKeyEntry helpers for clean data writing - insertKeyBTree: new root creation matches Harbour exactly - child[0] = newPage (left), child[1] = old root (right) NTX Traversal (ntx.go): - prevKey: guard iKey < keyCount before checking KeyChild (prevents infinite loop at rightmost child position) BOF Detection (indexer.go): - Set a.FBof AFTER GoTo returns (GoTo line 393 resets FBof=false) - Previously: set FBof before GoTo → immediately cleared Results: Unit tests ALL PASS, Stress test 82 items 79/82 match (96%) Remaining 3 diffs: duplicate key count edge case + SET DELETED seek Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -569,7 +569,6 @@ func (a *DBFArea) SkipIndexed(count int64) error {
|
||||
for i := int64(0); i > count; i-- {
|
||||
idx.SkipPrev()
|
||||
if idx.IsBOF() {
|
||||
a.FBof = true
|
||||
// Stay at first record in scope
|
||||
if a.idxState.scopeTop != nil {
|
||||
idx.Seek(a.idxState.scopeTop)
|
||||
@@ -577,9 +576,12 @@ func (a *DBFArea) SkipIndexed(count int64) error {
|
||||
idx.GoTop()
|
||||
}
|
||||
if !idx.IsEOF() {
|
||||
return a.GoTo(idx.CurRecNo())
|
||||
a.GoTo(idx.CurRecNo())
|
||||
} else {
|
||||
a.GoTo(1)
|
||||
}
|
||||
return a.GoTo(1)
|
||||
a.FBof = true // set AFTER GoTo (GoTo resets FBof)
|
||||
return nil
|
||||
}
|
||||
// Check top scope
|
||||
if hasScope && a.idxState.scopeTop != nil {
|
||||
|
||||
@@ -325,39 +325,31 @@ func (idx *Index) insertKeyBTree(key []byte, recNo uint32) error {
|
||||
}
|
||||
|
||||
// Split propagated to root — create new root
|
||||
// Harbour: new page with promoted key at pos 0
|
||||
// child[0] = promoteChild (newPage = LEFT half)
|
||||
// child[1] = old root (RIGHT half)
|
||||
newRootOff := int64(idx.header.NextPage)
|
||||
idx.header.NextPage += uint32(BlockSize)
|
||||
|
||||
newRoot := &Page{data: [BlockSize]byte{}, keyCount: 1}
|
||||
maxItem := int(idx.header.MaxItem)
|
||||
itemSize := int(idx.header.ItemSize)
|
||||
dataStart := 2 + (maxItem+1)*2
|
||||
|
||||
binary.LittleEndian.PutUint16(newRoot.data[0:2], 1)
|
||||
// Initialize offset table for all slots
|
||||
newRoot := &Page{data: [BlockSize]byte{}, keyCount: 0}
|
||||
for i := 0; i <= maxItem; i++ {
|
||||
binary.LittleEndian.PutUint16(newRoot.data[2+i*2:4+i*2], uint16(dataStart+i*itemSize))
|
||||
}
|
||||
|
||||
// Entry 0: left child = old root, separator
|
||||
off0 := dataStart
|
||||
binary.LittleEndian.PutUint16(newRoot.data[2:4], uint16(off0))
|
||||
binary.LittleEndian.PutUint32(newRoot.data[off0:off0+4], idx.header.Root) // old root
|
||||
binary.LittleEndian.PutUint32(newRoot.data[off0+4:off0+8], promoteRecNo)
|
||||
copy(newRoot.data[off0+8:off0+8+idx.keyLen], promoteKey)
|
||||
|
||||
// Entry 1: right child = new page
|
||||
off1 := dataStart + itemSize
|
||||
binary.LittleEndian.PutUint16(newRoot.data[4:6], uint16(off1))
|
||||
binary.LittleEndian.PutUint32(newRoot.data[off1:off1+4], promoteChild)
|
||||
// Insert promoted key at position 0 with child = promoteChild (left half)
|
||||
idx.pageInsertKey(newRoot, 0, promoteKey, promoteRecNo, promoteChild)
|
||||
// Set trailing child (position 1) = old root (right half)
|
||||
trailOff := int(newRoot.keyOffset(1))
|
||||
binary.LittleEndian.PutUint32(newRoot.data[trailOff:trailOff+4], idx.header.Root)
|
||||
|
||||
newRoot.writeTo(idx.file, newRootOff)
|
||||
idx.header.Root = uint32(newRootOff)
|
||||
|
||||
// Update header
|
||||
f := idx.file
|
||||
f.Seek(0, 0)
|
||||
WriteHeader(f, &idx.header)
|
||||
idx.file.Seek(0, 0)
|
||||
WriteHeader(idx.file, &idx.header)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -416,91 +408,119 @@ func (idx *Index) pageInsertKey(page *Page, iKey int, key []byte, recNo uint32,
|
||||
binary.LittleEndian.PutUint16(page.data[0:2], page.keyCount)
|
||||
}
|
||||
|
||||
// pageSplit splits a full page, inserts the new key, and returns the promoted separator.
|
||||
// pageSplit splits a full page — exact port of Harbour hb_ntxPageSplit.
|
||||
// NewPage = LEFT (lower half), OldPage = RIGHT (upper half, offset-swapped).
|
||||
// Returns: promoted key, its recNo, and newPage offset (left child of promoted key).
|
||||
func (idx *Index) pageSplit(page *Page, iKey int, key []byte, recNo uint32, childPage uint32, pageOff int64) ([]byte, uint32, uint32, error) {
|
||||
maxItem := int(idx.header.MaxItem)
|
||||
itemSize := int(idx.header.ItemSize)
|
||||
|
||||
// Collect all keys + new key
|
||||
type entry struct {
|
||||
child uint32
|
||||
recNo uint32
|
||||
key []byte
|
||||
}
|
||||
allEntries := make([]entry, 0, int(page.keyCount)+1)
|
||||
|
||||
for i := 0; i < int(page.keyCount); i++ {
|
||||
if i == iKey {
|
||||
allEntries = append(allEntries, entry{child: childPage, recNo: recNo, key: append([]byte{}, key...)})
|
||||
}
|
||||
allEntries = append(allEntries, entry{
|
||||
child: page.KeyChild(i),
|
||||
recNo: page.KeyRecNo(i),
|
||||
key: append([]byte{}, page.KeyValue(i, idx.keyLen)...),
|
||||
})
|
||||
}
|
||||
if iKey == int(page.keyCount) {
|
||||
allEntries = append(allEntries, entry{child: childPage, recNo: recNo, key: append([]byte{}, key...)})
|
||||
}
|
||||
// Trailing child
|
||||
trailingChild := page.KeyChild(int(page.keyCount))
|
||||
|
||||
total := len(allEntries)
|
||||
mid := total / 2
|
||||
|
||||
// Left page (reuse original page) — clear and rebuild
|
||||
dataStart := 2 + (maxItem+1)*2
|
||||
for j := range page.data {
|
||||
page.data[j] = 0
|
||||
}
|
||||
page.keyCount = 0
|
||||
binary.LittleEndian.PutUint16(page.data[0:2], 0)
|
||||
for i := 0; i <= maxItem; i++ {
|
||||
binary.LittleEndian.PutUint16(page.data[2+i*2:4+i*2], uint16(dataStart+i*itemSize))
|
||||
}
|
||||
for i := 0; i < mid; i++ {
|
||||
idx.pageInsertKey(page, i, allEntries[i].key, allEntries[i].recNo, allEntries[i].child)
|
||||
}
|
||||
// Set trailing child pointer
|
||||
trailOff := int(page.keyOffset(mid))
|
||||
binary.LittleEndian.PutUint32(page.data[trailOff:trailOff+4], allEntries[mid].child)
|
||||
page.writeTo(idx.file, pageOff)
|
||||
|
||||
// Promoted separator
|
||||
promKey := append([]byte{}, allEntries[mid].key...)
|
||||
promRecNo := allEntries[mid].recNo
|
||||
|
||||
// Right page (new page) — initialize offset table
|
||||
rightOff := int64(idx.header.NextPage)
|
||||
// Allocate new page (will be the LEFT / lower half)
|
||||
newPageOff := int64(idx.header.NextPage)
|
||||
idx.header.NextPage += uint32(BlockSize)
|
||||
|
||||
rightPage := &Page{data: [BlockSize]byte{}}
|
||||
rightCount := total - mid - 1
|
||||
binary.LittleEndian.PutUint16(rightPage.data[0:2], uint16(rightCount))
|
||||
newPage := &Page{data: [BlockSize]byte{}}
|
||||
// Initialize offset table
|
||||
for i := 0; i <= maxItem; i++ {
|
||||
binary.LittleEndian.PutUint16(rightPage.data[2+i*2:4+i*2], uint16(dataStart+i*itemSize))
|
||||
binary.LittleEndian.PutUint16(newPage.data[2+i*2:4+i*2], uint16(dataStart+i*itemSize))
|
||||
}
|
||||
rightPage.keyCount = 0
|
||||
for i := 0; i < rightCount; i++ {
|
||||
srcIdx := mid + 1 + i
|
||||
idx.pageInsertKey(rightPage, i, allEntries[srcIdx].key, allEntries[srcIdx].recNo, allEntries[srcIdx].child)
|
||||
|
||||
uiKeys := int(page.keyCount) + 1 // total including new key
|
||||
uiHalf := uiKeys / 2
|
||||
|
||||
// Phase 1: Copy lower half to newPage
|
||||
j := 0 // index into old page entries
|
||||
for newPageCount := 0; newPageCount < uiHalf; newPageCount++ {
|
||||
if newPageCount == iKey {
|
||||
// Insert the new key here
|
||||
idx.setKeyEntry(newPage, newPageCount, childPage, recNo, key)
|
||||
} else {
|
||||
// Copy from old page entry j
|
||||
idx.copyKeyEntry(newPage, newPageCount, page, j)
|
||||
j++
|
||||
}
|
||||
newPage.keyCount++
|
||||
}
|
||||
// Trailing child
|
||||
rightTrailOff := int(rightPage.keyOffset(rightCount))
|
||||
if mid+1+rightCount < len(allEntries) {
|
||||
binary.LittleEndian.PutUint32(rightPage.data[rightTrailOff:rightTrailOff+4], allEntries[mid+1+rightCount].child)
|
||||
binary.LittleEndian.PutUint16(newPage.data[0:2], newPage.keyCount)
|
||||
|
||||
// Phase 2: Extract promoted key (middle key)
|
||||
var promKey []byte
|
||||
var promRecNo uint32
|
||||
if uiHalf == iKey {
|
||||
// The new key IS the promoted separator
|
||||
promRecNo = recNo
|
||||
promKey = append([]byte{}, key...)
|
||||
// newPage's trailing child = the new key's child pointer
|
||||
trailOff := int(newPage.keyOffset(int(newPage.keyCount)))
|
||||
binary.LittleEndian.PutUint32(newPage.data[trailOff:trailOff+4], childPage)
|
||||
} else {
|
||||
binary.LittleEndian.PutUint32(rightPage.data[rightTrailOff:rightTrailOff+4], trailingChild)
|
||||
// Promoted key comes from old page entry j
|
||||
promRecNo = page.KeyRecNo(j)
|
||||
promKey = append([]byte{}, page.KeyValue(j, idx.keyLen)...)
|
||||
// newPage's trailing child = old page entry j's child
|
||||
trailOff := int(newPage.keyOffset(int(newPage.keyCount)))
|
||||
binary.LittleEndian.PutUint32(newPage.data[trailOff:trailOff+4], page.KeyChild(j))
|
||||
j++
|
||||
}
|
||||
rightPage.writeTo(idx.file, rightOff)
|
||||
|
||||
// Update header on disk
|
||||
f := idx.file
|
||||
f.Seek(0, 0)
|
||||
WriteHeader(f, &idx.header)
|
||||
// Phase 3: Upper half stays in old page (offset swapping — Harbour style)
|
||||
// Rearrange the old page's offset table so that entries j..keyCount-1
|
||||
// become positions 0..i-1, using offset swapping (no data copying).
|
||||
i := 0
|
||||
for uiHalf++; uiHalf < uiKeys; uiHalf++ {
|
||||
if uiHalf == iKey {
|
||||
// Insert new key at position i in old page
|
||||
idx.setKeyEntry(page, i, childPage, recNo, key)
|
||||
} else {
|
||||
// Swap offset[i] and offset[j] — data stays in place
|
||||
offI := page.keyOffset(i)
|
||||
offJ := page.keyOffset(j)
|
||||
binary.LittleEndian.PutUint16(page.data[2+j*2:4+j*2], offI)
|
||||
binary.LittleEndian.PutUint16(page.data[2+i*2:4+i*2], offJ)
|
||||
j++
|
||||
}
|
||||
i++
|
||||
}
|
||||
// Move trailing child: old page's last child → position i
|
||||
lastChild := page.KeyChild(int(page.keyCount))
|
||||
trailOff := int(page.keyOffset(int(page.keyCount)))
|
||||
binary.LittleEndian.PutUint32(page.data[trailOff:trailOff+4], 0) // clear old
|
||||
newTrailOff := int(page.keyOffset(i))
|
||||
binary.LittleEndian.PutUint32(page.data[newTrailOff:newTrailOff+4], lastChild)
|
||||
page.keyCount = uint16(i)
|
||||
binary.LittleEndian.PutUint16(page.data[0:2], page.keyCount)
|
||||
|
||||
return promKey, promRecNo, uint32(rightOff), nil
|
||||
// Write both pages
|
||||
newPage.writeTo(idx.file, newPageOff)
|
||||
page.writeTo(idx.file, pageOff)
|
||||
|
||||
// Update header
|
||||
idx.file.Seek(0, 0)
|
||||
WriteHeader(idx.file, &idx.header)
|
||||
|
||||
// Harbour: pKeyNew->Tag = pNewPage->Page (left child = newPage)
|
||||
return promKey, promRecNo, uint32(newPageOff), nil
|
||||
}
|
||||
|
||||
// setKeyEntry writes a key entry at position i in a page.
|
||||
func (idx *Index) setKeyEntry(page *Page, i int, child uint32, recNo uint32, key []byte) {
|
||||
off := int(page.keyOffset(i))
|
||||
binary.LittleEndian.PutUint32(page.data[off:off+4], child)
|
||||
binary.LittleEndian.PutUint32(page.data[off+4:off+8], recNo)
|
||||
padKey := make([]byte, idx.keyLen)
|
||||
for j := range padKey {
|
||||
padKey[j] = ' '
|
||||
}
|
||||
copy(padKey, key)
|
||||
copy(page.data[off+8:off+8+idx.keyLen], padKey)
|
||||
}
|
||||
|
||||
// copyKeyEntry copies a full key entry (child+recNo+key) from src page position srcI to dst position dstI.
|
||||
func (idx *Index) copyKeyEntry(dst *Page, dstI int, src *Page, srcI int) {
|
||||
itemSize := int(idx.header.ItemSize)
|
||||
dstOff := int(dst.keyOffset(dstI))
|
||||
srcOff := int(src.keyOffset(srcI))
|
||||
copy(dst.data[dstOff:dstOff+itemSize], src.data[srcOff:srcOff+itemSize])
|
||||
}
|
||||
|
||||
// writeTo writes a page to file at the given offset.
|
||||
|
||||
Reference in New Issue
Block a user