- Compiler: PP → Lexer → Parser → Analyzer → Gengo pipeline - Parser: 232/236 (98%) Harbour compatibility, registry-based dispatch - RTL: 351 Harbour-compatible functions - RDD: DBF/NTX/CDX engines with Rushmore bitmap optimization - Go Interop: IMPORT + pkg.Func() + obj:Method() with FastPath (15M calls/sec) - HB_FUNC API: Full Harbour C API compatible Go bridge - Concurrency: SPAWN/LAUNCH/GOROUTINE, <-, WATCH, PARALLEL FOR, ASYNC/AWAIT - Extensions: Multi-return, DEFER, Slice, f-string, Nil-safe ?:, CONST - Macro Compiler: Runtime AST parsing and evaluation - Debugger: TUI debugger with source display, breakpoints, stepping - FRB: Native + Pcode dual mode runtime binary - Tests: 13 packages ALL PASS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
605 lines
15 KiB
Go
605 lines
15 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
// NTX index engine for Five.
|
|
// B-tree index with 1024-byte pages, byte-compatible with Harbour/Clipper NTX files.
|
|
//
|
|
// Reference:
|
|
// /mnt/d/harbour-core/include/hbrddntx.h — structures
|
|
// /mnt/d/harbour-core/src/rdd/dbfntx/dbfntx1.c — algorithms
|
|
// docs/rdd-architecture-spec.md Section 6 — SEEK→Index chain
|
|
package ntx
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// NTX constants — matching Harbour exactly.
|
|
const (
|
|
BlockSize = 1024 // NTXBLOCKSIZE (1 << 10)
|
|
HeaderSize = 1024 // NTX header occupies first block
|
|
MaxKey = 256 // NTX_MAX_KEY
|
|
MaxExpr = 256 // NTX_MAX_EXP
|
|
MaxTagName = 10 // NTX_MAX_TAGNAME
|
|
StackSize = 32 // NTX_STACKSIZE
|
|
HdrUnused = 473 // NTX_HDR_UNUSED
|
|
|
|
IgnoreRecNum = 0x00000000 // NTX_IGNORE_REC_NUM
|
|
MaxRecNum = 0xFFFFFFFF // NTX_MAX_REC_NUM
|
|
)
|
|
|
|
// Header is the NTX file header (1024 bytes on disk).
|
|
// Harbour: NTXHEADER in hbrddntx.h:93
|
|
type Header struct {
|
|
Type uint16 // offset 0 (0x0401 = NTX)
|
|
Version uint16 // offset 2
|
|
Root uint32 // offset 4 (root page byte offset)
|
|
NextPage uint32 // offset 8 (next free page byte offset)
|
|
ItemSize uint16 // offset 12 (key entry size: 8 + keyLen)
|
|
KeySize uint16 // offset 14 (key value length)
|
|
KeyDec uint16 // offset 16
|
|
MaxItem uint16 // offset 18 (max keys per page)
|
|
HalfPage uint16 // offset 20
|
|
KeyExpr [MaxExpr]byte // offset 22
|
|
Unique byte // offset 278
|
|
Pad1 byte // offset 279
|
|
Descend byte // offset 280
|
|
Pad2 byte // offset 281
|
|
ForExpr [MaxExpr]byte // offset 282
|
|
TagName [MaxTagName+2]byte // offset 538
|
|
Custom byte // offset 550
|
|
Unused [HdrUnused]byte // offset 551
|
|
}
|
|
|
|
// ReadHeader reads the NTX header from a file.
|
|
func ReadHeader(f *os.File) (*Header, error) {
|
|
buf := make([]byte, HeaderSize)
|
|
if _, err := f.ReadAt(buf, 0); err != nil {
|
|
return nil, fmt.Errorf("read NTX header: %w", err)
|
|
}
|
|
h := &Header{}
|
|
r := bytes.NewReader(buf)
|
|
if err := binary.Read(r, binary.LittleEndian, h); err != nil {
|
|
return nil, err
|
|
}
|
|
return h, nil
|
|
}
|
|
|
|
// WriteHeader writes the NTX header to a file.
|
|
func WriteHeader(f *os.File, h *Header) error {
|
|
var buf bytes.Buffer
|
|
if err := binary.Write(&buf, binary.LittleEndian, h); err != nil {
|
|
return err
|
|
}
|
|
// Pad to BlockSize
|
|
pad := make([]byte, HeaderSize-buf.Len())
|
|
buf.Write(pad)
|
|
_, err := f.WriteAt(buf.Bytes(), 0)
|
|
return err
|
|
}
|
|
|
|
func (h *Header) GetKeyExpr() string { return trimNull(h.KeyExpr[:]) }
|
|
func (h *Header) GetForExpr() string { return trimNull(h.ForExpr[:]) }
|
|
func (h *Header) GetTagName() string { return trimNull(h.TagName[:]) }
|
|
|
|
// --- Page ---
|
|
|
|
// Page represents an NTX B-tree page (1024 bytes).
|
|
// Harbour: HB_PAGEINFO in hbrddntx.h:180
|
|
//
|
|
// On-disk layout:
|
|
// [keyCount: 2 bytes LE]
|
|
// [keyOffsets: (maxItem+1) * 2 bytes LE] — indices into key data area
|
|
// [key data area]
|
|
//
|
|
// Each key entry:
|
|
// [childPage: 4 bytes LE] — child page offset (0 = leaf)
|
|
// [recNo: 4 bytes LE] — record number
|
|
// [keyValue: keyLen bytes] — key data
|
|
type Page struct {
|
|
offset int64 // file offset of this page
|
|
data [BlockSize]byte
|
|
keyCount uint16
|
|
changed bool
|
|
}
|
|
|
|
// LoadPage reads a page from the file.
|
|
func LoadPage(f *os.File, offset int64) (*Page, error) {
|
|
p := &Page{offset: offset}
|
|
if _, err := f.ReadAt(p.data[:], offset); err != nil {
|
|
return nil, fmt.Errorf("read NTX page at %d: %w", offset, err)
|
|
}
|
|
p.keyCount = binary.LittleEndian.Uint16(p.data[0:2])
|
|
return p, nil
|
|
}
|
|
|
|
// WritePage writes a page to the file.
|
|
func WritePage(f *os.File, p *Page) error {
|
|
binary.LittleEndian.PutUint16(p.data[0:2], p.keyCount)
|
|
_, err := f.WriteAt(p.data[:], p.offset)
|
|
return err
|
|
}
|
|
|
|
// keyOffset returns the byte offset within the page for key at index i.
|
|
// The offset table starts at byte 2: each entry is 2 bytes LE.
|
|
func (p *Page) keyOffset(i int) uint16 {
|
|
off := 2 + i*2
|
|
if off+2 > len(p.data) {
|
|
return 0
|
|
}
|
|
return binary.LittleEndian.Uint16(p.data[off : off+2])
|
|
}
|
|
|
|
// KeyChild returns the child page offset for key at index i.
|
|
func (p *Page) KeyChild(i int) uint32 {
|
|
off := int(p.keyOffset(i))
|
|
if off+4 > len(p.data) {
|
|
return 0
|
|
}
|
|
return binary.LittleEndian.Uint32(p.data[off : off+4])
|
|
}
|
|
|
|
// KeyRecNo returns the record number for key at index i.
|
|
func (p *Page) KeyRecNo(i int) uint32 {
|
|
off := int(p.keyOffset(i)) + 4
|
|
if off+4 > len(p.data) {
|
|
return 0
|
|
}
|
|
return binary.LittleEndian.Uint32(p.data[off : off+4])
|
|
}
|
|
|
|
// KeyValue returns the key bytes for key at index i.
|
|
func (p *Page) KeyValue(i int, keyLen int) []byte {
|
|
off := p.keyOffset(i) + 8
|
|
return p.data[off : off+uint16(keyLen)]
|
|
}
|
|
|
|
// --- Stack entry for tree traversal ---
|
|
|
|
// StackEntry tracks position during B-tree traversal.
|
|
// Harbour: TREE_STACK in hbrddntx.h:173
|
|
type StackEntry struct {
|
|
PageOffset int64 // page file offset
|
|
KeyIndex int // key position within page
|
|
}
|
|
|
|
// --- Index file ---
|
|
|
|
// Index represents an open NTX index file.
|
|
type Index struct {
|
|
file *os.File
|
|
header Header
|
|
keyLen int
|
|
itemSize int // 8 + keyLen
|
|
|
|
// Current position
|
|
stack [StackSize]StackEntry
|
|
stackLevel int
|
|
curRecNo uint32
|
|
curKey []byte
|
|
tagBOF bool
|
|
tagEOF bool
|
|
|
|
// Tag properties
|
|
ascendKey bool
|
|
uniqueKey bool
|
|
keyType byte // 'C', 'N', 'D', 'L'
|
|
}
|
|
|
|
// OpenIndex opens an existing NTX index file.
|
|
func OpenIndex(path string) (*Index, error) {
|
|
if !strings.HasSuffix(strings.ToLower(path), ".ntx") {
|
|
path += ".ntx"
|
|
}
|
|
|
|
f, err := os.OpenFile(path, os.O_RDWR, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hdr, err := ReadHeader(f)
|
|
if err != nil {
|
|
f.Close()
|
|
return nil, err
|
|
}
|
|
|
|
idx := &Index{
|
|
file: f,
|
|
header: *hdr,
|
|
keyLen: int(hdr.KeySize),
|
|
itemSize: int(hdr.ItemSize),
|
|
ascendKey: hdr.Descend == 0,
|
|
uniqueKey: hdr.Unique != 0,
|
|
curKey: make([]byte, hdr.KeySize),
|
|
}
|
|
|
|
// Determine key type from expression (simplified)
|
|
idx.keyType = 'C' // default
|
|
|
|
return idx, nil
|
|
}
|
|
|
|
// Close closes the index file.
|
|
func (idx *Index) KeyLen() int { return idx.keyLen }
|
|
|
|
func (idx *Index) Close() error {
|
|
return idx.file.Close()
|
|
}
|
|
|
|
// --- SEEK: B-tree search ---
|
|
// Harbour: hb_ntxTagKeyFind in dbfntx1.c:2564
|
|
|
|
// Seek searches for a key in the B-tree.
|
|
// Returns (recordNumber, exactMatch).
|
|
// If not found: positions at next higher key (for SOFTSEEK).
|
|
func (idx *Index) Seek(searchKey []byte) (uint32, bool) {
|
|
idx.stackLevel = 0
|
|
idx.tagBOF = false
|
|
idx.tagEOF = false
|
|
|
|
pageOffset := int64(idx.header.Root)
|
|
|
|
// Phase 1: Traverse from root to leaf
|
|
for {
|
|
page, err := LoadPage(idx.file, pageOffset)
|
|
if err != nil {
|
|
idx.tagEOF = true
|
|
return 0, false
|
|
}
|
|
|
|
iKey, found := idx.pageKeyFind(page, searchKey, false, 0)
|
|
|
|
// Push onto stack
|
|
if idx.stackLevel < StackSize {
|
|
idx.stack[idx.stackLevel] = StackEntry{
|
|
PageOffset: pageOffset,
|
|
KeyIndex: iKey,
|
|
}
|
|
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
|
|
childOffset := page.KeyChild(iKey)
|
|
if childOffset == 0 {
|
|
// At leaf — no exact match
|
|
// Position at this key (next higher) for SOFTSEEK
|
|
if iKey < int(page.keyCount) {
|
|
idx.curRecNo = page.KeyRecNo(iKey)
|
|
copy(idx.curKey, page.KeyValue(iKey, idx.keyLen))
|
|
} else {
|
|
// Past end of page — try next via stack
|
|
if idx.nextKey() {
|
|
return idx.curRecNo, false
|
|
}
|
|
idx.tagEOF = true
|
|
idx.curRecNo = 0
|
|
}
|
|
return idx.curRecNo, false
|
|
}
|
|
|
|
pageOffset = int64(childOffset)
|
|
}
|
|
}
|
|
|
|
// pageKeyFind performs binary search within a page.
|
|
// Harbour: hb_ntxPageKeyFind in dbfntx1.c:2497
|
|
// Returns (keyIndex, exactMatch).
|
|
func (idx *Index) pageKeyFind(page *Page, searchKey []byte, fNext bool, recNo uint32) (int, bool) {
|
|
lo, hi := 0, int(page.keyCount)-1
|
|
found := false
|
|
last := -1
|
|
|
|
for lo <= hi {
|
|
mid := (lo + hi) / 2
|
|
cmp := idx.compareKeys(searchKey, page.KeyValue(mid, idx.keyLen))
|
|
|
|
// Descending index: flip comparison
|
|
if cmp != 0 && !idx.ascendKey {
|
|
cmp = -cmp
|
|
}
|
|
|
|
if fNext && cmp >= 0 || !fNext && cmp > 0 {
|
|
lo = mid + 1
|
|
} else {
|
|
if cmp == 0 && recNo == 0 {
|
|
found = true
|
|
}
|
|
last = mid
|
|
hi = mid - 1
|
|
}
|
|
}
|
|
|
|
if last >= 0 {
|
|
return last, found
|
|
}
|
|
return int(page.keyCount), found
|
|
}
|
|
|
|
// compareKeys compares two key values.
|
|
// Harbour: hb_ntxValCompare in dbfntx1.c:679
|
|
// Returns: -1, 0, +1
|
|
func (idx *Index) compareKeys(key1, key2 []byte) int {
|
|
limit := len(key1)
|
|
if len(key2) < limit {
|
|
limit = len(key2)
|
|
}
|
|
|
|
cmp := bytes.Compare(key1[:limit], key2[:limit])
|
|
if cmp != 0 {
|
|
if cmp > 0 {
|
|
return 1
|
|
}
|
|
return -1
|
|
}
|
|
|
|
if len(key1) > len(key2) {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// --- SKIP: navigate through index ---
|
|
|
|
// nextKey moves to the next key in index order.
|
|
// Harbour: hb_ntxTagNextKey in dbfntx1.c:2387
|
|
//
|
|
// NTX B-tree traversal:
|
|
// key[i] has left-child at KeyChild(i) and right-child at KeyChild(i+1).
|
|
// After visiting key[i], the next key is the leftmost key in KeyChild(i+1),
|
|
// or if no child, key[i+1] in same page, or walk up to parent.
|
|
func (idx *Index) nextKey() bool {
|
|
if idx.stackLevel == 0 {
|
|
return false
|
|
}
|
|
|
|
level := idx.stackLevel - 1
|
|
page, err := LoadPage(idx.file, idx.stack[level].PageOffset)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
iKey := idx.stack[level].KeyIndex
|
|
|
|
// Check right child of current key: KeyChild(iKey+1)
|
|
if iKey+1 <= int(page.keyCount) {
|
|
childOff := page.KeyChild(iKey + 1)
|
|
if childOff != 0 {
|
|
// Has right child — go to its leftmost leaf
|
|
idx.stack[level].KeyIndex = iKey + 1
|
|
return idx.goLeftmost(int64(childOff))
|
|
}
|
|
}
|
|
|
|
// No right child — try next key in same page
|
|
if iKey+1 < int(page.keyCount) {
|
|
idx.stack[level].KeyIndex = iKey + 1
|
|
idx.curRecNo = page.KeyRecNo(iKey + 1)
|
|
copy(idx.curKey, page.KeyValue(iKey+1, idx.keyLen))
|
|
return true
|
|
}
|
|
|
|
// End of page — walk up the stack
|
|
// When ascending, stack[level].KeyIndex points to the child we descended into.
|
|
// The next unvisited key in the parent is at that same KeyIndex
|
|
// (it's the separator AFTER the child). But if we descended via KeyChild(iKey+1)
|
|
// at line 377 (setting KeyIndex=iKey+1), then on ascent that separator was already
|
|
// visited before descending. So we need to check if the key at KeyIndex has been
|
|
// visited (recNo matches curRecNo) and skip if so.
|
|
for level > 0 {
|
|
level--
|
|
page, err = LoadPage(idx.file, idx.stack[level].PageOffset)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
ki := idx.stack[level].KeyIndex
|
|
if ki < int(page.keyCount) {
|
|
recNo := page.KeyRecNo(ki)
|
|
if recNo != 0 && recNo != idx.curRecNo {
|
|
// This key hasn't been visited yet
|
|
idx.stackLevel = level + 1
|
|
idx.curRecNo = recNo
|
|
copy(idx.curKey, page.KeyValue(ki, idx.keyLen))
|
|
return true
|
|
}
|
|
// Already visited — advance and try next
|
|
idx.stack[level].KeyIndex = ki + 1
|
|
if ki+1 < int(page.keyCount) {
|
|
// Check right child first
|
|
childOff := page.KeyChild(ki + 1)
|
|
if childOff != 0 {
|
|
idx.stack[level].KeyIndex = ki + 1
|
|
idx.stackLevel = level + 1
|
|
return idx.goLeftmost(int64(childOff))
|
|
}
|
|
idx.stackLevel = level + 1
|
|
idx.curRecNo = page.KeyRecNo(ki + 1)
|
|
copy(idx.curKey, page.KeyValue(ki+1, idx.keyLen))
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false // EOF
|
|
}
|
|
|
|
// prevKey moves to the previous key in index order.
|
|
// Harbour: hb_ntxTagPrevKey in dbfntx1.c:2432
|
|
func (idx *Index) prevKey() bool {
|
|
if idx.stackLevel == 0 {
|
|
return false
|
|
}
|
|
|
|
level := idx.stackLevel - 1
|
|
page, err := LoadPage(idx.file, idx.stack[level].PageOffset)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
iKey := idx.stack[level].KeyIndex
|
|
|
|
// Check child at current position
|
|
childOff := page.KeyChild(iKey)
|
|
if childOff != 0 {
|
|
return idx.goRightmost(int64(childOff))
|
|
}
|
|
|
|
if iKey > 0 {
|
|
// Previous key in same page
|
|
idx.stack[level].KeyIndex = iKey - 1
|
|
idx.curRecNo = page.KeyRecNo(iKey - 1)
|
|
copy(idx.curKey, page.KeyValue(iKey-1, idx.keyLen))
|
|
return true
|
|
}
|
|
|
|
// Walk up
|
|
for level > 0 {
|
|
level--
|
|
page, err = LoadPage(idx.file, idx.stack[level].PageOffset)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if idx.stack[level].KeyIndex > 0 {
|
|
idx.stack[level].KeyIndex--
|
|
idx.stackLevel = level + 1
|
|
idx.curRecNo = page.KeyRecNo(idx.stack[level].KeyIndex)
|
|
copy(idx.curKey, page.KeyValue(idx.stack[level].KeyIndex, idx.keyLen))
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false // BOF
|
|
}
|
|
|
|
// goLeftmost traverses to the leftmost (smallest) key from a page.
|
|
func (idx *Index) goLeftmost(pageOffset int64) bool {
|
|
for {
|
|
page, err := LoadPage(idx.file, pageOffset)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
if idx.stackLevel < StackSize {
|
|
idx.stack[idx.stackLevel] = StackEntry{PageOffset: pageOffset, KeyIndex: 0}
|
|
idx.stackLevel++
|
|
}
|
|
|
|
childOff := page.KeyChild(0)
|
|
if childOff == 0 {
|
|
// Leaf reached
|
|
if page.keyCount > 0 {
|
|
idx.curRecNo = page.KeyRecNo(0)
|
|
copy(idx.curKey, page.KeyValue(0, idx.keyLen))
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
pageOffset = int64(childOff)
|
|
}
|
|
}
|
|
|
|
// goRightmost traverses to the rightmost (largest) key from a page.
|
|
func (idx *Index) goRightmost(pageOffset int64) bool {
|
|
for {
|
|
page, err := LoadPage(idx.file, pageOffset)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
lastKey := int(page.keyCount) - 1
|
|
if idx.stackLevel < StackSize {
|
|
idx.stack[idx.stackLevel] = StackEntry{PageOffset: pageOffset, KeyIndex: lastKey}
|
|
idx.stackLevel++
|
|
}
|
|
|
|
// Try rightmost child (at keyCount position)
|
|
childOff := page.KeyChild(int(page.keyCount))
|
|
if childOff == 0 {
|
|
if lastKey >= 0 {
|
|
idx.curRecNo = page.KeyRecNo(lastKey)
|
|
copy(idx.curKey, page.KeyValue(lastKey, idx.keyLen))
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
pageOffset = int64(childOff)
|
|
}
|
|
}
|
|
|
|
// GoTop positions at the first key in index order.
|
|
func (idx *Index) GoTop() bool {
|
|
idx.stackLevel = 0
|
|
idx.tagBOF = false
|
|
idx.tagEOF = false
|
|
return idx.goLeftmost(int64(idx.header.Root))
|
|
}
|
|
|
|
// GoBottom positions at the last key in index order.
|
|
func (idx *Index) GoBottom() bool {
|
|
idx.stackLevel = 0
|
|
idx.tagBOF = false
|
|
idx.tagEOF = false
|
|
return idx.goRightmost(int64(idx.header.Root))
|
|
}
|
|
|
|
// SkipNext moves to the next key. Returns false at EOF.
|
|
func (idx *Index) SkipNext() bool {
|
|
idx.tagBOF = false
|
|
if idx.stackLevel == 0 {
|
|
idx.tagEOF = true
|
|
return false
|
|
}
|
|
if !idx.nextKey() {
|
|
idx.tagEOF = true
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// SkipPrev moves to the previous key. Returns false at BOF.
|
|
func (idx *Index) SkipPrev() bool {
|
|
idx.tagEOF = false
|
|
if idx.stackLevel == 0 {
|
|
idx.tagBOF = true
|
|
return false
|
|
}
|
|
if !idx.prevKey() {
|
|
idx.tagBOF = true
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// CurRecNo returns the current record number.
|
|
func (idx *Index) CurRecNo() uint32 { return idx.curRecNo }
|
|
|
|
// CurKey returns the current key value.
|
|
func (idx *Index) CurKey() []byte { return idx.curKey[:idx.keyLen] }
|
|
|
|
// IsEOF returns true if past end of index.
|
|
func (idx *Index) IsEOF() bool { return idx.tagEOF }
|
|
|
|
// IsBOF returns true if before start of index.
|
|
func (idx *Index) IsBOF() bool { return idx.tagBOF }
|
|
|
|
// --- Helpers ---
|
|
|
|
func trimNull(b []byte) string {
|
|
for i, c := range b {
|
|
if c == 0 {
|
|
return strings.TrimSpace(string(b[:i]))
|
|
}
|
|
}
|
|
return strings.TrimSpace(string(b))
|
|
}
|