Files
five/hbrdd/ntx/build.go
Charles KWON OhJun ad544a5528 fix: Windows cross-compilation support (GOOS=windows)
- debugcli.go/debugtui.go: add //go:build !windows tag
- debugcli_windows.go/debugtui_windows.go: no-op stubs
- cdx/cdx.go: extract mmap to platform-specific files
- cdx/mmap_posix.go: syscall.Mmap/Munmap
- cdx/mmap_windows.go: no-op (falls back to read)
- ntx/ntx.go, ntx/build.go: same mmap extraction
- ntx/mmap_posix.go, ntx/mmap_windows.go: platform split

Builds verified: linux/amd64, windows/amd64, darwin/arm64, darwin/amd64

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:23:52 +09:00

827 lines
24 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// NTX index creation (INDEX ON) and key insertion.
package ntx
import (
"encoding/binary"
"fmt"
"os"
"sort"
)
// KeyRecord pairs a key value with its record number for sorting.
type KeyRecord struct {
Key []byte
RecNo uint32
}
// CreateIndex builds a new NTX index file from sorted key-record pairs.
func CreateIndex(path string, keyExpr string, keyLen int, unique bool, descend bool, keys []KeyRecord) (*Index, error) {
if len(path) < 4 || path[len(path)-4:] != ".ntx" {
path += ".ntx"
}
f, err := os.Create(path)
if err != nil {
return nil, fmt.Errorf("create NTX %s: %w", path, err)
}
itemSize := 8 + keyLen
maxItem := calculateMaxItems(itemSize)
halfPage := maxItem / 2
// Proper B-tree bulk build — works for any depth.
// Strategy: split sorted keys into groups of maxItem with separators between.
// keys[0..M-1] → leaf, keys[M] → separator, keys[M+1..2M] → leaf, ...
// Each separator exists ONLY in the parent, never in a leaf.
var buf pageBuffer
buf.init()
var rootOffset uint32
if len(keys) == 0 {
off := buf.allocPage()
initPageOffsets(buf.getPage(off), maxItem, itemSize)
rootOffset = uint32(off)
} else {
rootOffset = uint32(bulkBuildBTree(&buf, keys, keyLen, maxItem, itemSize))
}
nextPage := uint32(int64(HeaderSize) + int64(buf.count)*BlockSize)
// Write header
hdr := Header{
Type: 0x0401,
Version: 1,
Root: rootOffset,
NextPage: nextPage,
ItemSize: uint16(itemSize),
KeySize: uint16(keyLen),
KeyDec: 0,
MaxItem: uint16(maxItem),
HalfPage: uint16(halfPage),
}
copy(hdr.KeyExpr[:], keyExpr)
if unique {
hdr.Unique = 1
}
if descend {
hdr.Descend = 1
}
if err := WriteHeader(f, &hdr); err != nil {
f.Close()
return nil, err
}
// Bulk write all pages at correct offset (after header)
if buf.count > 0 {
if _, err := f.WriteAt(buf.data[:int64(buf.count)*BlockSize], int64(HeaderSize)); err != nil {
f.Close()
return nil, fmt.Errorf("bulk write NTX pages: %w", err)
}
}
f.Close()
return OpenIndex(path)
}
// bulkBuildBTree builds a proper B-tree in memory from sorted keys.
// Returns the file offset of the root page.
// Algorithm: distribute keys into leaf groups with separators extracted between them.
// [leaf0: M keys] [sep0] [leaf1: M keys] [sep1] ... [leafN: remaining keys]
// Then recursively build interior level from (leaf offsets + separator keys).
func bulkBuildBTree(buf *pageBuffer, keys []KeyRecord, keyLen, maxItem, itemSize int) int64 {
if len(keys) <= maxItem {
// Single leaf — base case
return buildOnePage(buf, keys, keyLen, maxItem, itemSize, nil)
}
// Split keys into leaf groups + separators
type childInfo struct {
offset int64
sepKey []byte // separator AFTER this child (nil for last)
sepRec uint32
}
// Pre-size to avoid slice growth during leaf splitting.
children := make([]childInfo, 0, len(keys)/maxItem+2)
i := 0
for i < len(keys) {
end := i + maxItem
if end > len(keys) {
end = len(keys)
}
// Prevent orphan: if taking maxItem keys leaves only 1 key for separator+next leaf,
// adjust so the last two leaves share keys evenly.
if end < len(keys) {
remaining := len(keys) - end
if remaining <= 2 {
if end-i+remaining <= maxItem {
// Absorb: total fits in one page
end = len(keys)
} else {
// Split evenly: give half of remaining+current to each of last 2 leaves.
// Total keys for last 2 leaves + 1 separator = len(keys) - i
total := len(keys) - i
end = i + (total-1)/2 // -1 accounts for separator between them
}
}
}
chunk := keys[i:end]
leafOff := buildOnePage(buf, chunk, keyLen, maxItem, itemSize, nil)
ci := childInfo{offset: leafOff}
i = end
// Extract separator only if 2+ keys remain (1 for sep + 1+ for next leaf)
if i < len(keys) && i+1 < len(keys) {
// At least 1 more key after separator → safe to promote.
// Reference the source key directly (caller's slab allocation is
// keyLen-aligned from OrderCreate's fast path, so no padding copy
// is needed). For slow path, the key was already padded in-place.
if len(keys[i].Key) == keyLen {
ci.sepKey = keys[i].Key
} else {
ci.sepKey = make([]byte, keyLen)
padCopy(ci.sepKey, keys[i].Key, keyLen)
}
ci.sepRec = keys[i].RecNo
i++ // skip separator key — it goes to parent only
}
children = append(children, ci)
}
// Build interior levels bottom-up
for len(children) > 1 {
var nextChildren []childInfo
j := 0
for j < len(children) {
// Collect up to maxItem+1 children for one parent page
end := j + maxItem + 1
if end > len(children) {
end = len(children)
}
group := children[j:end]
// Build parent page: separators come from group[0..n-2].sepKey
nKeys := len(group) - 1
parentOff := buf.allocPage()
pg := buf.getPage(parentOff)
initPageOffsets(pg, maxItem, itemSize)
binary.LittleEndian.PutUint16(pg[0:2], uint16(nKeys))
for k := 0; k <= nKeys; k++ {
entOff := int(binary.LittleEndian.Uint16(pg[2+k*2 : 4+k*2]))
binary.LittleEndian.PutUint32(pg[entOff:entOff+4], uint32(group[k].offset))
if k < nKeys && group[k].sepKey != nil {
binary.LittleEndian.PutUint32(pg[entOff+4:entOff+8], group[k].sepRec)
copy(pg[entOff+8:entOff+8+keyLen], group[k].sepKey)
}
}
// This parent becomes a child for the next level
ci := childInfo{offset: parentOff}
// Separator for this parent = last group member's separator
// (the separator that would follow this parent's range)
if end < len(children) {
// Use the last child's separator as the parent's separator
ci.sepKey = group[nKeys].sepKey
ci.sepRec = group[nKeys].sepRec
}
nextChildren = append(nextChildren, ci)
j = end
}
children = nextChildren
}
return children[0].offset
}
// buildOnePage creates a single leaf or interior page with the given keys.
// Zero-allocation: writes padded keys directly into the page buffer.
func buildOnePage(buf *pageBuffer, keys []KeyRecord, keyLen, maxItem, itemSize int, childOffsets []int64) int64 {
off := buf.allocPage()
pg := buf.getPage(off)
initPageOffsets(pg, maxItem, itemSize)
for i, kr := range keys {
entOff := int(binary.LittleEndian.Uint16(pg[2+i*2 : 4+i*2]))
if childOffsets != nil && i < len(childOffsets) {
binary.LittleEndian.PutUint32(pg[entOff:entOff+4], uint32(childOffsets[i]))
} else {
binary.LittleEndian.PutUint32(pg[entOff:entOff+4], 0) // leaf
}
binary.LittleEndian.PutUint32(pg[entOff+4:entOff+8], kr.RecNo)
// Write padded key directly into page buffer (no intermediate alloc).
padCopy(pg[entOff+8:entOff+8+keyLen], kr.Key, keyLen)
}
binary.LittleEndian.PutUint16(pg[0:2], uint16(len(keys)))
return off
}
// padCopy copies src into dst, padding with spaces. Zero extra allocation.
func padCopy(dst, src []byte, keyLen int) {
n := copy(dst, src)
for i := n; i < keyLen; i++ {
dst[i] = ' '
}
}
// rebuildWithInsert creates an NTX using per-key insertion (proper B-tree).
// Used for 3+ level trees where bulk build has separator duplication issues.
func rebuildWithInsert(path, keyExpr string, keyLen int, unique, descend bool, keys []KeyRecord) (*Index, error) {
itemSize := 8 + keyLen
maxItem := calculateMaxItems(itemSize)
halfPage := maxItem / 2
f, err := os.Create(path)
if err != nil {
return nil, err
}
rootOff := int64(HeaderSize)
emptyRoot := [BlockSize]byte{}
initPageOffsets(emptyRoot[:], maxItem, itemSize)
hdr := Header{
Type: 0x0401, Version: 1,
Root: uint32(rootOff), NextPage: uint32(rootOff + BlockSize),
ItemSize: uint16(itemSize), KeySize: uint16(keyLen),
MaxItem: uint16(maxItem), HalfPage: uint16(halfPage),
}
copy(hdr.KeyExpr[:], keyExpr)
if unique {
hdr.Unique = 1
}
if descend {
hdr.Descend = 1
}
WriteHeader(f, &hdr)
f.WriteAt(emptyRoot[:], rootOff)
f.Close()
idx, err := OpenIndex(path)
if err != nil {
return nil, err
}
for _, kr := range keys {
k := make([]byte, keyLen)
copy(k, kr.Key)
if err := idx.insertKeyBTree(k, kr.RecNo); err != nil {
idx.Close()
return nil, err
}
}
// Re-enable mmap now that file is complete
idx.mmapFile()
return idx, nil
}
// --- Bulk build buffer (ported from rddfive/ntx_engine.c) ---
type pageBuffer struct {
data []byte
count int
capacity int
}
func (pb *pageBuffer) init() {
pb.capacity = 64
pb.data = make([]byte, int64(pb.capacity)*BlockSize)
}
func (pb *pageBuffer) grow() {
if pb.count >= pb.capacity {
newCap := pb.capacity * 2
newData := make([]byte, int64(newCap)*BlockSize)
copy(newData, pb.data[:int64(pb.count)*BlockSize])
pb.data = newData
pb.capacity = newCap
}
}
// allocPage allocates a new page and returns its file offset.
func (pb *pageBuffer) allocPage() int64 {
pb.grow()
off := int64(HeaderSize) + int64(pb.count)*BlockSize
pb.count++
return off
}
// getPage returns a slice into the buffer for the page at the given file offset.
func (pb *pageBuffer) getPage(fileOffset int64) []byte {
idx := (fileOffset - int64(HeaderSize)) / BlockSize
start := idx * BlockSize
return pb.data[start : start+BlockSize]
}
// initPageOffsets pre-initializes the offset table for a page.
func initPageOffsets(pg []byte, maxItem, itemSize int) {
dataStart := 2 + (maxItem+1)*2
for i := 0; i <= maxItem; i++ {
binary.LittleEndian.PutUint16(pg[2+i*2:4+i*2], uint16(dataStart+i*itemSize))
}
}
// --- Internal build structures (legacy) ---
type buildPage struct {
data [BlockSize]byte
fileOffset int64
keyCount int
firstKey []byte
firstRecNo uint32
}
func calculateMaxItems(itemSize int) int {
// Page layout: [keyCount:2] [offsets:(max+1)*2] [entries:(max+1)*itemSize]
// Total must fit in BlockSize (1024).
// 2 + (max+1)*2 + (max+1)*itemSize <= 1024
// (max+1) * (itemSize + 2) <= 1022
// max+1 <= 1022 / (itemSize + 2)
// max <= 1022/(itemSize+2) - 1
max := 1022/(itemSize+2) - 1
if max < 2 {
max = 2
}
if max > 250 {
max = 250
}
return max
}
func makeEmptyPage(keyLen, itemSize, maxItem int, offset int64) *buildPage {
pg := &buildPage{
fileOffset: offset,
firstKey: make([]byte, keyLen),
}
binary.LittleEndian.PutUint16(pg.data[0:2], 0)
return pg
}
func buildLeafPages(keys []KeyRecord, keyLen, itemSize, maxItem int, nextOffset *int64) []*buildPage {
if len(keys) == 0 {
return nil
}
var pages []*buildPage
for i := 0; i < len(keys); i += maxItem {
end := i + maxItem
if end > len(keys) {
end = len(keys)
}
chunk := keys[i:end]
pg := encodeLeafPage(chunk, keyLen, itemSize, maxItem, *nextOffset)
*nextOffset += BlockSize
pages = append(pages, pg)
}
return pages
}
func encodeLeafPage(keys []KeyRecord, keyLen, itemSize, maxItem int, offset int64) *buildPage {
pg := &buildPage{
fileOffset: offset,
keyCount: len(keys),
firstKey: make([]byte, keyLen),
firstRecNo: keys[0].RecNo,
}
copy(pg.firstKey, keys[0].Key)
binary.LittleEndian.PutUint16(pg.data[0:2], uint16(len(keys)))
dataStart := 2 + (maxItem+1)*2
for i, kr := range keys {
entryOffset := dataStart + i*itemSize
binary.LittleEndian.PutUint16(pg.data[2+i*2:4+i*2], uint16(entryOffset))
binary.LittleEndian.PutUint32(pg.data[entryOffset:entryOffset+4], 0) // leaf
binary.LittleEndian.PutUint32(pg.data[entryOffset+4:entryOffset+8], kr.RecNo)
key := make([]byte, keyLen)
for j := range key {
key[j] = ' '
}
copy(key, kr.Key)
copy(pg.data[entryOffset+8:entryOffset+8+keyLen], key)
}
// Trailing offset
binary.LittleEndian.PutUint16(
pg.data[2+len(keys)*2:4+len(keys)*2],
uint16(dataStart+len(keys)*itemSize),
)
return pg
}
func buildInternalLevel(children []*buildPage, keyLen, itemSize, maxItem int, nextOffset *int64) []*buildPage {
var pages []*buildPage
// Each internal page holds up to maxItem keys and maxItem+1 child pointers.
// So each parent page covers maxItem+1 children.
fanout := maxItem + 1
for i := 0; i < len(children); i += fanout {
end := i + fanout
if end > len(children) {
end = len(children)
}
chunk := children[i:end]
pg := encodeInternalPage(chunk, keyLen, itemSize, maxItem, *nextOffset)
*nextOffset += BlockSize
pages = append(pages, pg)
}
return pages
}
func encodeInternalPage(children []*buildPage, keyLen, itemSize, maxItem int, offset int64) *buildPage {
nKeys := len(children) - 1
if nKeys < 0 {
nKeys = 0
}
pg := &buildPage{
fileOffset: offset,
keyCount: nKeys,
}
if len(children) > 0 {
pg.firstKey = make([]byte, keyLen)
copy(pg.firstKey, children[0].firstKey)
pg.firstRecNo = children[0].firstRecNo
}
binary.LittleEndian.PutUint16(pg.data[0:2], uint16(nKeys))
dataStart := 2 + (maxItem+1)*2
for i := 0; i <= nKeys; i++ {
entryOffset := dataStart + i*itemSize
if entryOffset+itemSize > BlockSize {
// Page overflow — cap keys
binary.LittleEndian.PutUint16(pg.data[0:2], uint16(i))
break
}
binary.LittleEndian.PutUint16(pg.data[2+i*2:4+i*2], uint16(entryOffset))
// Child pointer
binary.LittleEndian.PutUint32(pg.data[entryOffset:entryOffset+4],
uint32(children[i].fileOffset))
if i < nKeys {
// Separator = first key of child[i+1], promoted (not duplicated)
binary.LittleEndian.PutUint32(pg.data[entryOffset+4:entryOffset+8],
children[i+1].firstRecNo)
key := make([]byte, keyLen)
for j := range key {
key[j] = ' '
}
copy(key, children[i+1].firstKey)
copy(pg.data[entryOffset+8:entryOffset+8+keyLen], key)
} else {
binary.LittleEndian.PutUint32(pg.data[entryOffset+4:entryOffset+8], 0)
}
}
return pg
}
// --- B-tree insertion ---
// insertKeyBTree inserts a single key into the B-tree with proper page splitting.
// Harbour: hb_ntxTagKeyAdd in dbfntx1.c
func (idx *Index) insertKeyBTree(key []byte, recNo uint32) error {
// Disable mmap during insertion (file grows, mmap stale)
if idx.mmapData != nil {
munmapFile(idx.mmapData)
idx.mmapData = nil
}
// Search for insertion position
idx.stackLevel = 0
pageOff := int64(idx.header.Root)
for {
page, err := LoadPage(idx.file, pageOff)
if err != nil {
return err
}
iKey := idx.insertSearch(page, key, recNo)
if idx.stackLevel < StackSize {
idx.stack[idx.stackLevel] = StackEntry{PageOffset: pageOff, KeyIndex: iKey}
idx.stackLevel++
}
childOff := page.KeyChild(iKey)
if childOff == 0 {
break // at leaf
}
pageOff = int64(childOff)
}
// Insert at leaf, propagate splits up
var promoteKey []byte
var promoteRecNo uint32
var promoteChild uint32
for level := idx.stackLevel - 1; level >= 0; level-- {
page, err := LoadPage(idx.file, idx.stack[level].PageOffset)
if err != nil {
return err
}
iKey := idx.stack[level].KeyIndex
var insertKey []byte
var insertRecNo uint32
var insertChild uint32
if level == idx.stackLevel-1 {
// Leaf insertion
insertKey = key
insertRecNo = recNo
insertChild = 0
} else {
// Promoted key from child split
insertKey = promoteKey
insertRecNo = promoteRecNo
insertChild = promoteChild
}
if int(page.keyCount) < int(idx.header.MaxItem) {
// Page has room — insert directly
idx.pageInsertKey(page, iKey, insertKey, insertRecNo, insertChild)
page.writeTo(idx.file, idx.stack[level].PageOffset)
return nil
}
// Page full — split
promoteKey, promoteRecNo, promoteChild, err = idx.pageSplit(page, iKey, insertKey, insertRecNo, insertChild, idx.stack[level].PageOffset)
if err != nil {
return err
}
}
// 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)
maxItem := int(idx.header.MaxItem)
itemSize := int(idx.header.ItemSize)
dataStart := 2 + (maxItem+1)*2
newRoot := &Page{data: make([]byte, BlockSize), keyCount: 0}
for i := 0; i <= maxItem; i++ {
binary.LittleEndian.PutUint16(newRoot.data[2+i*2:4+i*2], uint16(dataStart+i*itemSize))
}
// 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)
idx.file.Seek(0, 0)
WriteHeader(idx.file, &idx.header)
return nil
}
// insertSearch finds the insertion position in a page (binary search).
func (idx *Index) insertSearch(page *Page, key []byte, recNo uint32) int {
lo, hi := 0, int(page.keyCount)-1
for lo <= hi {
mid := (lo + hi) / 2
cmp := idx.compareKeys(key, page.KeyValue(mid, idx.keyLen))
if cmp == 0 {
// Equal keys: sort by recNo
midRec := page.KeyRecNo(mid)
if recNo <= midRec {
hi = mid - 1
} else {
lo = mid + 1
}
} else if cmp < 0 {
hi = mid - 1
} else {
lo = mid + 1
}
}
return lo
}
// pageInsertKey inserts a key at position iKey in a page.
// Harbour: hb_ntxPageKeyAdd — swaps offsets, writes key data at freed slot.
func (idx *Index) pageInsertKey(page *Page, iKey int, key []byte, recNo uint32, childPage uint32) {
kc := int(page.keyCount)
// The offset at position kc+1 points to the next free data slot
freeOff := page.keyOffset(kc + 1)
// Shift offset table right: move [iKey..kc] to [iKey+1..kc+1]
for i := kc + 1; i > iKey; i-- {
prev := page.keyOffset(i - 1)
binary.LittleEndian.PutUint16(page.data[2+i*2:4+i*2], prev)
}
// Put the free slot offset at position iKey
binary.LittleEndian.PutUint16(page.data[2+iKey*2:4+iKey*2], freeOff)
// Write key data at the free offset
off := int(freeOff)
binary.LittleEndian.PutUint32(page.data[off:off+4], childPage)
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)
page.keyCount++
binary.LittleEndian.PutUint16(page.data[0:2], page.keyCount)
}
// 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)
dataStart := 2 + (maxItem+1)*2
// Allocate new page (will be the LEFT / lower half)
newPageOff := int64(idx.header.NextPage)
idx.header.NextPage += uint32(BlockSize)
newPage := &Page{data: make([]byte, BlockSize)}
// Initialize offset table
for i := 0; i <= maxItem; i++ {
binary.LittleEndian.PutUint16(newPage.data[2+i*2:4+i*2], uint16(dataStart+i*itemSize))
}
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++
}
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 {
// 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++
}
// 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)
// 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. Zero extra allocation.
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)
dest := page.data[off+8 : off+8+idx.keyLen]
n := copy(dest, key)
for j := n; j < idx.keyLen; j++ {
dest[j] = ' '
}
}
// 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.
func (p *Page) writeTo(f *os.File, offset int64) {
f.WriteAt(p.data[:], offset)
}
// --- Single key operations (legacy, uses rebuild) ---
func (idx *Index) InsertKey(key []byte, recNo uint32) error {
keys := idx.collectAllKeys()
newKey := make([]byte, idx.keyLen)
copy(newKey, key)
keys = append(keys, KeyRecord{Key: newKey, RecNo: recNo})
sort.Slice(keys, func(i, j int) bool {
cmp := idx.compareKeys(keys[i].Key, keys[j].Key)
if cmp == 0 {
return keys[i].RecNo < keys[j].RecNo
}
return cmp < 0
})
path := idx.file.Name()
idx.Close()
newIdx, err := CreateIndex(path, idx.header.GetKeyExpr(), idx.keyLen,
idx.uniqueKey, !idx.ascendKey, keys)
if err != nil {
return err
}
*idx = *newIdx
return nil
}
func (idx *Index) DeleteKey(recNo uint32) error {
keys := idx.collectAllKeys()
filtered := keys[:0]
for _, kr := range keys {
if kr.RecNo != recNo {
filtered = append(filtered, kr)
}
}
path := idx.file.Name()
idx.Close()
newIdx, err := CreateIndex(path, idx.header.GetKeyExpr(), idx.keyLen,
idx.uniqueKey, !idx.ascendKey, filtered)
if err != nil {
return err
}
*idx = *newIdx
return nil
}
func (idx *Index) collectAllKeys() []KeyRecord {
var keys []KeyRecord
if !idx.GoTop() {
return keys
}
keys = append(keys, KeyRecord{
Key: append([]byte{}, idx.curKey[:idx.keyLen]...),
RecNo: idx.curRecNo,
})
for idx.SkipNext() {
keys = append(keys, KeyRecord{
Key: append([]byte{}, idx.curKey[:idx.keyLen]...),
RecNo: idx.curRecNo,
})
}
return keys
}