Files
five/hbrdd/ntx/ntx.go
Charles KWON OhJun 59568f3301 Five v0.9 — Harbour + Go fusion language
- 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>
2026-03-31 09:41:50 +09:00

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))
}