Critical fix: REPLACE was calling area.Flush() after every field write! - gengo gen_cmd.go: removed Flush() from emitReplaceCmd - Harbour defers write until DBCOMMIT/CLOSE/GoTo, not per-REPLACE Combined with bulk build + deferred APPEND: - B1 APPEND 10K: 72,228ms → 30ms (2,400x improvement!) - B2 INDEX NAME: 34ms → 5ms (6.8x improvement) - Harbour comparison: Five 30ms vs Harbour 27ms (1.1x) Also: OrderCreate flushes dirty record + EOF + header before index build Benchmark on ext4 (home dir): ┌─────────────┬──────────┬────────┬───────┐ │ Benchmark │ Harbour │ Five │ Ratio │ ├─────────────┼──────────┼────────┼───────┤ │ APPEND 10K │ 27ms │ 30ms │ 1.1x │ │ INDEX NAME │ 2ms │ 5ms │ 2.5x │ │ INDEX CITY │ 0ms │ 7ms │ - │ │ SEEK 10K │ 6ms │ 25ms │ 4.2x │ │ SCAN FWD │ 1ms │ 6ms │ 6x │ │ SCAN BWD │ 0ms │ 6ms │ - │ │ PACK │ 4ms │ 3ms │ 0.75x │ └─────────────┴──────────┴────────┴───────┘ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
715 lines
20 KiB
Go
715 lines
20 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
|
|
|
|
// Bulk build: all pages in memory buffer, single write at end.
|
|
// Ported from rddfive/ntx_engine.c fa_ntx_create().
|
|
var buf pageBuffer
|
|
buf.init()
|
|
|
|
var rootOffset uint32
|
|
|
|
if len(keys) == 0 {
|
|
// Empty index: single empty root page
|
|
off := buf.allocPage()
|
|
initPageOffsets(buf.getPage(off), maxItem, itemSize)
|
|
rootOffset = uint32(off)
|
|
} else {
|
|
// Phase 1: Build leaf pages in memory
|
|
var leafOffsets []int64
|
|
keyIdx := 0
|
|
for keyIdx < len(keys) {
|
|
off := buf.allocPage()
|
|
pg := buf.getPage(off)
|
|
initPageOffsets(pg, maxItem, itemSize)
|
|
|
|
cnt := 0
|
|
for cnt < maxItem && keyIdx < len(keys) {
|
|
entOff := int(binary.LittleEndian.Uint16(pg[2+cnt*2 : 4+cnt*2]))
|
|
binary.LittleEndian.PutUint32(pg[entOff:entOff+4], 0) // child=0 (leaf)
|
|
binary.LittleEndian.PutUint32(pg[entOff+4:entOff+8], keys[keyIdx].RecNo)
|
|
padded := make([]byte, keyLen)
|
|
for j := range padded {
|
|
padded[j] = ' '
|
|
}
|
|
copy(padded, keys[keyIdx].Key)
|
|
copy(pg[entOff+8:entOff+8+keyLen], padded)
|
|
cnt++
|
|
keyIdx++
|
|
}
|
|
binary.LittleEndian.PutUint16(pg[0:2], uint16(cnt))
|
|
leafOffsets = append(leafOffsets, off)
|
|
}
|
|
|
|
// Phase 2: Build interior levels bottom-up (B-tree: promote last key from child)
|
|
curLevel := leafOffsets
|
|
isLeafLevel := true
|
|
for len(curLevel) > 1 {
|
|
var nextLevel []int64
|
|
for i := 0; i < len(curLevel); i += maxItem + 1 {
|
|
end := i + maxItem + 1
|
|
if end > len(curLevel) {
|
|
end = len(curLevel)
|
|
}
|
|
children := curLevel[i:end]
|
|
nKeys := len(children) - 1
|
|
|
|
// Promote: extract last key from each child[0..nKeys-1] as separator
|
|
// Only remove from LEAF pages (interior keys are already promoted separators)
|
|
type sepInfo struct {
|
|
recNo uint32
|
|
key []byte
|
|
}
|
|
seps := make([]sepInfo, nKeys)
|
|
for j := 0; j < nKeys; j++ {
|
|
childPg := buf.getPage(children[j])
|
|
childCnt := int(binary.LittleEndian.Uint16(childPg[0:2]))
|
|
if childCnt > 0 {
|
|
lastIdx := childCnt - 1
|
|
lastOff := int(binary.LittleEndian.Uint16(childPg[2+lastIdx*2 : 4+lastIdx*2]))
|
|
seps[j].recNo = binary.LittleEndian.Uint32(childPg[lastOff+4 : lastOff+8])
|
|
seps[j].key = make([]byte, keyLen)
|
|
copy(seps[j].key, childPg[lastOff+8:lastOff+8+keyLen])
|
|
// Remove from leaf only (interior separators stay as routing keys)
|
|
if isLeafLevel {
|
|
binary.LittleEndian.PutUint16(childPg[0:2], uint16(childCnt-1))
|
|
}
|
|
}
|
|
}
|
|
|
|
off := buf.allocPage()
|
|
pg := buf.getPage(off)
|
|
initPageOffsets(pg, maxItem, itemSize)
|
|
binary.LittleEndian.PutUint16(pg[0:2], uint16(nKeys))
|
|
|
|
for j := 0; j <= nKeys; j++ {
|
|
entOff := int(binary.LittleEndian.Uint16(pg[2+j*2 : 4+j*2]))
|
|
// Child pointer
|
|
binary.LittleEndian.PutUint32(pg[entOff:entOff+4], uint32(children[j]))
|
|
if j < nKeys {
|
|
// Separator from promoted keys
|
|
binary.LittleEndian.PutUint32(pg[entOff+4:entOff+8], seps[j].recNo)
|
|
copy(pg[entOff+8:entOff+8+keyLen], seps[j].key)
|
|
}
|
|
}
|
|
nextLevel = append(nextLevel, off)
|
|
}
|
|
curLevel = nextLevel
|
|
isLeafLevel = false
|
|
}
|
|
rootOffset = uint32(curLevel[0])
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// --- 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 {
|
|
// 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: [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))
|
|
}
|
|
// 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: [BlockSize]byte{}}
|
|
// 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.
|
|
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.
|
|
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
|
|
}
|