Files
five/hbrdd/dbf/indexer.go
Charles KWON OhJun b7028791d6 fix: 5 seek/dbf bugs — 77/77 thorough Harbour compatibility
1. SOFTSEEK: use idx.CurRecNo() for positioning (was checking recNo > 0)
   - SEEK with SET SOFTSEEK ON now positions at next higher key
   - SEEK command reads SET SOFTSEEK at runtime (was compile-time only)
   - rtlDbSeek defaults to GetSetSoftSeek() when no explicit param

2. SET DELETED ON + INDEX: SkipIndexed skips deleted records
   - GoTopIndexed: skip deleted record at top position
   - SkipIndexed: inner loop continues past deleted records

3. Compound key (CITY+NAME): field name TrimSpace before lookup
   - evalKeyExprInner: TrimSpace on fieldName after FIELD-> strip
   - Fixed "CITY " != "CITY" mismatch from + operator splitting

4. SET INDEX TO filename: treated as string, not variable
   - gengo uses exprToString for SET INDEX TO (was emitExpr)
   - Prevents identifier being resolved as local variable

5. hasXBaseCommands: recursive scan into nested blocks
   - BEGIN SEQUENCE, IF, FOR, DO WHILE, SWITCH bodies now scanned
   - Fixes missing hbrdd import for DB commands inside blocks

Thorough test: 77 items (14 sections) covering exact/partial/soft seek,
SET DELETED, duplicate keys, numeric keys, compound keys, empty/single
table, state consistency, order switching, full traversal — all identical.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:08:51 +09:00

1080 lines
27 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// DBFArea Indexer integration — connects NTX/CDX index engines to DBFArea.
// Implements hbrdd.Indexer interface on DBFArea.
package dbf
import (
"bytes"
"five/hbrt"
"five/hbrdd"
"five/hbrdd/cdx"
"five/hbrdd/ntx"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
// IndexEngine is the common interface for NTX Index and CDX Tag.
type IndexEngine interface {
Seek(searchKey []byte) (uint32, bool)
GoTop() bool
GoBottom() bool
SkipNext() bool
SkipPrev() bool
CurRecNo() uint32
CurKey() []byte
IsEOF() bool
IsBOF() bool
KeyLen() int
Close() error
}
// indexState holds active index state for a DBFArea.
type indexState struct {
indexes []IndexEngine // open NTX/CDX index engines
names []string // index file paths
tags []string // tag names (for display)
current int // active index (-1 = natural order)
keyExprs []string // key expressions for each index
// Scope support
scopeTop []byte // top scope key (nil = no scope)
scopeBottom []byte // bottom scope key (nil = no scope)
}
// KeyEvalFunc is a callback for evaluating index key expressions via the VM.
// Set by the generated code (via SetKeyEval) before calling OrderCreate.
// This allows evalKeyExprInner to call UDFs and evaluate complex expressions.
// Signature: func(exprString) → Value (called on the current Thread)
var KeyEvalFunc func(expr string) hbrt.Value
// ensureIndexState initializes the index state if nil.
func (a *DBFArea) ensureIndexState() {
if a.idxState == nil {
a.idxState = &indexState{current: -1}
}
}
// OrderCreate creates a new index file. Equivalent to INDEX ON.
func (a *DBFArea) OrderCreate(params hbrdd.OrderCreateParams) error {
a.ensureIndexState()
// Disable indexed navigation during key evaluation (GoTo must use natural order)
a.idxState.current = -1
idxPath := params.FilePath
if idxPath == "" {
return fmt.Errorf("index file path required")
}
// Ensure .ntx extension
if !strings.Contains(filepath.Base(idxPath), ".") {
idxPath += ".ntx"
}
// Build key evaluator from expression
keyExpr := strings.ToUpper(params.KeyExpr)
// Determine key length from first record (or default)
keyLen := 10
recCount, _ := a.RecCount()
if recCount > 0 {
sample := a.evalKeyExpr(keyExpr, 1)
if len(sample) > 0 {
keyLen = len(sample)
}
}
// Build key records — apply FOR condition if present
forExpr := strings.TrimSpace(params.ForExpr)
keys := make([]ntx.KeyRecord, 0, recCount)
for r := uint32(1); r <= recCount; r++ {
// FOR condition: skip records that don't match
if forExpr != "" {
if !a.evalForExpr(forExpr, r) {
continue
}
}
k := a.evalKeyExpr(keyExpr, r)
// Pad or trim to keyLen
if len(k) < keyLen {
padded := make([]byte, keyLen)
copy(padded, k)
for j := len(k); j < keyLen; j++ {
padded[j] = ' '
}
k = padded
} else if len(k) > keyLen {
k = k[:keyLen]
}
keys = append(keys, ntx.KeyRecord{Key: k, RecNo: r})
}
// Sort keys before building index
// Harbour: equal keys ordered by RecNo ascending (stable by record number)
sort.Slice(keys, func(i, j int) bool {
cmp := bytes.Compare(keys[i].Key, keys[j].Key)
if cmp == 0 {
return keys[i].RecNo < keys[j].RecNo
}
if params.Descending {
return cmp > 0
}
return cmp < 0
})
idx, err := ntx.CreateIndex(idxPath, keyExpr, keyLen, params.Unique, params.Descending, keys)
if err != nil {
return fmt.Errorf("create index failed: %w", err)
}
a.idxState.indexes = append(a.idxState.indexes, idx)
a.idxState.names = append(a.idxState.names, idxPath)
a.idxState.tags = append(a.idxState.tags, params.TagName)
a.idxState.keyExprs = append(a.idxState.keyExprs, keyExpr)
a.idxState.current = len(a.idxState.indexes) - 1
return nil
}
// OrderListAdd opens an existing index file (NTX single-order or CDX compound).
func (a *DBFArea) OrderListAdd(path string) error {
a.ensureIndexState()
// Auto-detect extension: try .cdx first, then .ntx
if !strings.Contains(filepath.Base(path), ".") {
if _, err := os.Stat(path + ".cdx"); err == nil {
path += ".cdx"
} else {
path += ".ntx"
}
}
ext := strings.ToLower(filepath.Ext(path))
if ext == ".cdx" {
// CDX compound index — opens all tags
ci, err := cdx.OpenIndex(path)
if err != nil {
return fmt.Errorf("open CDX failed: %w", err)
}
for _, tag := range ci.Tags() {
a.idxState.indexes = append(a.idxState.indexes, tag)
a.idxState.names = append(a.idxState.names, path)
a.idxState.tags = append(a.idxState.tags, tag.Name)
a.idxState.keyExprs = append(a.idxState.keyExprs, tag.KeyExpr())
}
if len(ci.Tags()) > 0 {
a.idxState.current = len(a.idxState.indexes) - len(ci.Tags()) // first tag
}
return nil
}
// NTX single index
idx, err := ntx.OpenIndex(path)
if err != nil {
return fmt.Errorf("open index failed: %w", err)
}
a.idxState.indexes = append(a.idxState.indexes, idx)
a.idxState.names = append(a.idxState.names, path)
a.idxState.tags = append(a.idxState.tags, "")
a.idxState.keyExprs = append(a.idxState.keyExprs, "")
a.idxState.current = len(a.idxState.indexes) - 1
return nil
}
// OrderListClear closes all index files.
func (a *DBFArea) OrderListClear() error {
if a.idxState == nil {
return nil
}
for _, idx := range a.idxState.indexes {
idx.Close()
}
a.idxState = &indexState{current: -1}
return nil
}
// OrderListFocus sets the active index by tag name, number, or file name.
// Harbour: OrdSetFocus(nOrder) or OrdSetFocus("tagName")
func (a *DBFArea) OrderListFocus(tagName string) error {
a.ensureIndexState()
if tagName == "" || tagName == "0" {
a.idxState.current = -1 // natural order
a.ClearScope()
return nil
}
// Try as numeric order (1-based)
if n, err := parseOrderNum(tagName); err == nil {
if n == 0 {
a.idxState.current = -1
a.ClearScope()
return nil
}
if n >= 1 && n <= len(a.idxState.indexes) {
a.idxState.current = n - 1
a.ClearScope()
return nil
}
}
upper := strings.ToUpper(tagName)
// Match by tag name
for i, name := range a.idxState.tags {
if strings.ToUpper(name) == upper {
a.idxState.current = i
a.ClearScope()
return nil
}
}
// Match by file name
for i, name := range a.idxState.names {
base := strings.ToUpper(filepath.Base(name))
ext := strings.ToUpper(filepath.Ext(name))
if base == upper || strings.TrimSuffix(base, ext) == upper {
a.idxState.current = i
a.ClearScope()
return nil
}
}
return fmt.Errorf("index not found: %s", tagName)
}
// parseOrderNum tries to parse a string as a positive integer (order number).
func parseOrderNum(s string) (int, error) {
s = strings.TrimSpace(s)
if len(s) == 0 {
return 0, fmt.Errorf("empty")
}
n := 0
for _, c := range s {
if c < '0' || c > '9' {
return 0, fmt.Errorf("not a number")
}
n = n*10 + int(c-'0')
}
return n, nil
}
// OrderListRebuild rebuilds all indexes.
// Harbour: ORDLISTREBUILD / REINDEX — recreates all open indexes from current data.
func (a *DBFArea) OrderListRebuild() error {
if a.idxState == nil || len(a.idxState.indexes) == 0 {
return nil
}
// Save current index info
savedCurrent := a.idxState.current
type idxInfo struct {
name string
tag string
keyExpr string
}
infos := make([]idxInfo, len(a.idxState.indexes))
for i := range a.idxState.indexes {
infos[i] = idxInfo{
name: a.idxState.names[i],
tag: a.idxState.tags[i],
keyExpr: a.idxState.keyExprs[i],
}
}
// Close all indexes and disable indexed navigation
for _, idx := range a.idxState.indexes {
idx.Close()
}
a.idxState.indexes = nil
a.idxState.names = nil
a.idxState.tags = nil
a.idxState.keyExprs = nil
a.idxState.current = -1
// Remove idxState so GoTo uses natural order during rebuild
a.idxState = nil
// Recreate each index
for _, info := range infos {
err := a.OrderCreate(hbrdd.OrderCreateParams{
KeyExpr: info.keyExpr,
FilePath: info.name,
TagName: info.tag,
})
if err != nil {
return fmt.Errorf("rebuild index %s: %w", info.name, err)
}
}
// Restore active index
if a.idxState != nil && savedCurrent >= 0 && savedCurrent < len(a.idxState.indexes) {
a.idxState.current = savedCurrent
}
return nil
}
// OrderDestroy removes an index file.
func (a *DBFArea) OrderDestroy(tagName string) error {
a.ensureIndexState()
upper := strings.ToUpper(tagName)
for i, name := range a.idxState.tags {
if strings.ToUpper(name) == upper {
a.idxState.indexes[i].Close()
os.Remove(a.idxState.names[i])
// Remove from slices
a.idxState.indexes = append(a.idxState.indexes[:i], a.idxState.indexes[i+1:]...)
a.idxState.names = append(a.idxState.names[:i], a.idxState.names[i+1:]...)
a.idxState.tags = append(a.idxState.tags[:i], a.idxState.tags[i+1:]...)
a.idxState.keyExprs = append(a.idxState.keyExprs[:i], a.idxState.keyExprs[i+1:]...)
if a.idxState.current >= len(a.idxState.indexes) {
a.idxState.current = -1
}
return nil
}
}
return fmt.Errorf("index not found: %s", tagName)
}
// OrderInfo returns information about an index order.
func (a *DBFArea) OrderInfo(ordNo int) (*hbrdd.OrderInfo, error) {
a.ensureIndexState()
idx := ordNo - 1
if idx < 0 || idx >= len(a.idxState.indexes) {
return nil, fmt.Errorf("invalid order number: %d", ordNo)
}
return &hbrdd.OrderInfo{
Name: a.idxState.tags[idx],
KeyExpr: a.idxState.keyExprs[idx],
}, nil
}
// Seek searches for a key in the active index.
// Harbour compatible: partial key matching, softseek, space padding.
func (a *DBFArea) Seek(key hbrt.Value, softSeek bool, findLast bool) (bool, error) {
a.ensureIndexState()
if a.idxState.current < 0 || a.idxState.current >= len(a.idxState.indexes) {
return false, fmt.Errorf("no active index")
}
idx := a.idxState.indexes[a.idxState.current]
keyLen := idx.KeyLen()
// Convert key to bytes and track actual search length
var searchKey []byte
var actualLen int
if key.IsString() {
s := key.AsString()
actualLen = len(s)
// Pad with spaces to full key length (Harbour convention)
if actualLen < keyLen {
padded := make([]byte, keyLen)
copy(padded, []byte(s))
for i := actualLen; i < keyLen; i++ {
padded[i] = ' '
}
searchKey = padded
} else {
searchKey = []byte(s[:keyLen])
actualLen = keyLen
}
} else if key.IsNumeric() {
s := fmt.Sprintf("%*d", keyLen, key.AsNumInt())
searchKey = []byte(s)
if len(searchKey) > keyLen {
searchKey = searchKey[:keyLen]
}
actualLen = keyLen
} else {
searchKey = []byte(key.AsString())
actualLen = len(searchKey)
}
// Seek in index
recNo, exactFound := idx.Seek(searchKey)
// If not exact, check partial match: compare only actualLen bytes
if !exactFound && recNo > 0 && actualLen < keyLen {
// Position at the found location and check partial match
curKey := idx.CurKey()
if len(curKey) >= actualLen && bytes.Equal(curKey[:actualLen], searchKey[:actualLen]) {
exactFound = true
}
}
if exactFound && recNo > 0 {
a.GoTo(recNo)
a.FEof = false
a.SetFound(true)
return true, nil
}
if softSeek && !idx.IsEOF() {
// Softseek: position at the next higher key
posRecNo := idx.CurRecNo()
if posRecNo > 0 {
a.GoTo(posRecNo)
a.FEof = false
a.SetFound(false)
return false, nil
}
}
// Not found — go to EOF
rc, _ := a.RecCount()
a.GoTo(rc + 1)
a.FEof = true
a.SetFound(false)
return false, nil
}
// GoTopIndexed positions at the first key in the active index.
// Harbour: if SCOPE is set, positions at the first key >= scopeTop.
func (a *DBFArea) GoTopIndexed() error {
if a.idxState == nil || a.idxState.current < 0 {
return a.GoTop()
}
idx := a.idxState.indexes[a.idxState.current]
if a.idxState.scopeTop != nil {
// Seek to scope top boundary
recNo, _ := idx.Seek(a.idxState.scopeTop)
if recNo == 0 || idx.IsEOF() {
rc, _ := a.RecCount()
a.FEof = true
return a.GoTo(rc + 1)
}
// Check if within bottom scope
if a.idxState.scopeBottom != nil {
if bytes.Compare(idx.CurKey(), a.idxState.scopeBottom) > 0 {
rc, _ := a.RecCount()
a.FEof = true
return a.GoTo(rc + 1)
}
}
return a.GoTo(idx.CurRecNo())
}
idx.GoTop()
if idx.IsEOF() {
rc, _ := a.RecCount()
a.FEof = true
return a.GoTo(rc + 1)
}
a.GoTo(idx.CurRecNo())
// Skip deleted records at top
if hbrdd.IsSetDeleted != nil && hbrdd.IsSetDeleted() && a.Deleted() {
return a.SkipIndexed(1)
}
return nil
}
// GoBottomIndexed positions at the last key in the active index.
// Harbour: if SCOPE is set, positions at the last key <= scopeBottom.
func (a *DBFArea) GoBottomIndexed() error {
if a.idxState == nil || a.idxState.current < 0 {
return a.GoBottom()
}
idx := a.idxState.indexes[a.idxState.current]
if a.idxState.scopeBottom != nil {
// Seek to scope bottom boundary
_, exact := idx.Seek(a.idxState.scopeBottom)
if idx.IsEOF() {
// All keys less than bottom scope — go to physical bottom
idx.GoBottom()
} else if !exact {
// Positioned past bottom — go back one
idx.SkipPrev()
} else {
// Exact match — skip forward to last matching key, then position there
for {
idx.SkipNext()
if idx.IsEOF() || bytes.Compare(idx.CurKey(), a.idxState.scopeBottom) > 0 {
idx.SkipPrev()
break
}
}
}
if idx.IsBOF() || idx.IsEOF() {
a.FBof = true
return a.GoTo(1)
}
// Verify within top scope
if a.idxState.scopeTop != nil {
if bytes.Compare(idx.CurKey(), a.idxState.scopeTop) < 0 {
a.FEof = true
rc, _ := a.RecCount()
return a.GoTo(rc + 1)
}
}
return a.GoTo(idx.CurRecNo())
}
idx.GoBottom()
if idx.IsBOF() {
return a.GoTo(1)
}
return a.GoTo(idx.CurRecNo())
}
// SkipIndexed skips using the active index order.
// Harbour: respects SCOPE boundaries — stops at scope edges.
func (a *DBFArea) SkipIndexed(count int64) error {
if a.idxState == nil || a.idxState.current < 0 {
return a.Skip(count)
}
idx := a.idxState.indexes[a.idxState.current]
hasScope := a.idxState.scopeTop != nil || a.idxState.scopeBottom != nil
setDel := hbrdd.IsSetDeleted != nil && hbrdd.IsSetDeleted()
if count > 0 {
for i := int64(0); i < count; i++ {
for {
idx.SkipNext()
if idx.IsEOF() || idx.CurRecNo() == 0 {
rc, _ := a.RecCount()
a.GoTo(rc + 1)
a.FEof = true
return nil
}
// Check bottom scope
if hasScope && a.idxState.scopeBottom != nil {
if bytes.Compare(idx.CurKey(), a.idxState.scopeBottom) > 0 {
rc, _ := a.RecCount()
a.GoTo(rc + 1)
a.FEof = true
return nil
}
}
// Skip deleted records
if setDel {
a.GoTo(idx.CurRecNo())
if a.Deleted() {
continue
}
}
break
}
}
} else if count < 0 {
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)
} else {
idx.GoTop()
}
if !idx.IsEOF() {
return a.GoTo(idx.CurRecNo())
}
return a.GoTo(1)
}
// Check top scope
if hasScope && a.idxState.scopeTop != nil {
if bytes.Compare(idx.CurKey(), a.idxState.scopeTop) < 0 {
a.FBof = true
idx.Seek(a.idxState.scopeTop)
if !idx.IsEOF() {
return a.GoTo(idx.CurRecNo())
}
return a.GoTo(1)
}
}
}
}
return a.GoTo(idx.CurRecNo())
}
// --- Scope support (ORDSCOPE) ---
// SetScope sets top and/or bottom scope boundaries for the active index.
// Harbour: OrdScope(TOPSCOPE, val) / OrdScope(BOTTOMSCOPE, val)
// Pass zero-value hbrt.Value{} (not MakeNil) to skip setting that boundary.
func (a *DBFArea) SetScope(top, bottom hbrt.Value) error {
a.ensureIndexState()
if a.idxState.current < 0 {
return fmt.Errorf("no active index")
}
idx := a.idxState.indexes[a.idxState.current]
keyLen := idx.KeyLen()
if !top.IsNil() && top.Type() != 0 {
a.idxState.scopeTop = scopeKeyFromValue(top, keyLen)
}
if !bottom.IsNil() && bottom.Type() != 0 {
a.idxState.scopeBottom = scopeKeyFromValue(bottom, keyLen)
}
return nil
}
// SetScopeTop sets only the top scope.
func (a *DBFArea) SetScopeTop(val hbrt.Value) {
a.ensureIndexState()
if a.idxState.current < 0 {
return
}
keyLen := a.idxState.indexes[a.idxState.current].KeyLen()
a.idxState.scopeTop = scopeKeyFromValue(val, keyLen)
}
// SetScopeBottom sets only the bottom scope.
func (a *DBFArea) SetScopeBottom(val hbrt.Value) {
a.ensureIndexState()
if a.idxState.current < 0 {
return
}
keyLen := a.idxState.indexes[a.idxState.current].KeyLen()
a.idxState.scopeBottom = scopeKeyFromValue(val, keyLen)
}
// ClearScope removes all scope boundaries.
func (a *DBFArea) ClearScope() error {
if a.idxState != nil {
a.idxState.scopeTop = nil
a.idxState.scopeBottom = nil
}
return nil
}
// ClearScopeTop removes only the top scope boundary.
func (a *DBFArea) ClearScopeTop() {
if a.idxState != nil {
a.idxState.scopeTop = nil
}
}
// ClearScopeBottom removes only the bottom scope boundary.
func (a *DBFArea) ClearScopeBottom() {
if a.idxState != nil {
a.idxState.scopeBottom = nil
}
}
// GetScopeTop returns the current top scope key (nil if none).
func (a *DBFArea) GetScopeTop() []byte {
if a.idxState != nil {
return a.idxState.scopeTop
}
return nil
}
// GetScopeBottom returns the current bottom scope key (nil if none).
func (a *DBFArea) GetScopeBottom() []byte {
if a.idxState != nil {
return a.idxState.scopeBottom
}
return nil
}
// scopeKeyFromValue converts a Harbour Value to a scope key byte slice.
func scopeKeyFromValue(v hbrt.Value, keyLen int) []byte {
var key []byte
if v.IsString() {
key = []byte(v.AsString())
} else if v.IsNumeric() {
key = []byte(fmt.Sprintf("%*d", keyLen, v.AsNumInt()))
} else {
key = []byte(v.AsString())
}
// Pad to keyLen
if len(key) < keyLen {
padded := make([]byte, keyLen)
copy(padded, key)
for i := len(key); i < keyLen; i++ {
padded[i] = ' '
}
return padded
}
if len(key) > keyLen {
return key[:keyLen]
}
return key
}
// --- Index info accessors ---
// IndexCount returns the number of open indexes.
func (a *DBFArea) IndexCount() int {
if a.idxState == nil {
return 0
}
return len(a.idxState.indexes)
}
// CurrentOrder returns the 1-based current order number (0 = natural).
func (a *DBFArea) CurrentOrder() int {
if a.idxState == nil || a.idxState.current < 0 {
return 0
}
return a.idxState.current + 1
}
// OrderName returns the tag name for order n (1-based).
func (a *DBFArea) OrderName(n int) string {
if a.idxState == nil || n < 1 || n > len(a.idxState.tags) {
return ""
}
return a.idxState.tags[n-1]
}
// OrderKeyExpr returns the key expression for order n (1-based).
func (a *DBFArea) OrderKeyExpr(n int) string {
if a.idxState == nil || n < 1 || n > len(a.idxState.keyExprs) {
return ""
}
return a.idxState.keyExprs[n-1]
}
// evalKeyExpr evaluates an index key expression for a given record.
// Supports: field names, UPPER(), LOWER(), LTRIM(), RTRIM(), ALLTRIM(),
// STR(), DTOS(), SUBSTR(), LEFT(), RIGHT(), PADL(), PADR(),
// field1+field2 (concatenation), nested functions.
func (a *DBFArea) evalKeyExpr(expr string, recNo uint32) []byte {
oldRecNo := a.recNo
a.GoTo(recNo)
result := a.evalKeyExprInner(strings.TrimSpace(expr))
a.GoTo(oldRecNo)
return result
}
func (a *DBFArea) evalKeyExprInner(expr string) []byte {
upper := strings.ToUpper(expr)
// String literal
if len(expr) >= 2 && expr[0] == '"' && expr[len(expr)-1] == '"' {
return []byte(expr[1 : len(expr)-1])
}
// Strip FIELD-> or _FIELD-> or alias-> prefix (Harbour: M->var, FIELD->var)
fieldName := strings.TrimSpace(upper)
if idx := strings.Index(fieldName, "->"); idx >= 0 {
fieldName = strings.TrimSpace(fieldName[idx+2:])
}
// Simple field name
for i := 0; i < a.FieldCount(); i++ {
fi := a.GetFieldInfo(i)
if strings.ToUpper(fi.Name) == fieldName {
val, _ := a.GetValue(i)
return formatKeyValue(val, fi)
}
}
// Function calls: FUNC(args)
if parenOpen := strings.Index(expr, "("); parenOpen > 0 {
funcName := strings.ToUpper(strings.TrimSpace(expr[:parenOpen]))
// Find matching close paren
parenClose := findMatchingParen(expr, parenOpen)
if parenClose < 0 {
parenClose = len(expr) - 1
}
argsStr := expr[parenOpen+1 : parenClose]
switch funcName {
case "UPPER":
inner := a.evalKeyExprInner(argsStr)
return []byte(strings.ToUpper(string(inner)))
case "LOWER":
inner := a.evalKeyExprInner(argsStr)
return []byte(strings.ToLower(string(inner)))
case "ALLTRIM", "TRIM":
inner := a.evalKeyExprInner(argsStr)
return []byte(strings.TrimSpace(string(inner)))
case "LTRIM":
inner := a.evalKeyExprInner(argsStr)
return []byte(strings.TrimLeft(string(inner), " "))
case "RTRIM":
inner := a.evalKeyExprInner(argsStr)
return []byte(strings.TrimRight(string(inner), " "))
case "LEFT":
args := splitArgs(argsStr)
if len(args) >= 2 {
inner := a.evalKeyExprInner(args[0])
n := parseIntIdx(args[1])
if n > len(inner) {
n = len(inner)
}
return inner[:n]
}
case "RIGHT":
args := splitArgs(argsStr)
if len(args) >= 2 {
inner := a.evalKeyExprInner(args[0])
n := parseIntIdx(args[1])
if n > len(inner) {
n = len(inner)
}
return inner[len(inner)-n:]
}
case "SUBSTR":
args := splitArgs(argsStr)
if len(args) >= 2 {
inner := a.evalKeyExprInner(args[0])
start := parseIntIdx(args[1]) - 1 // 1-based to 0-based
if start < 0 {
start = 0
}
length := len(inner) - start
if len(args) >= 3 {
length = parseIntIdx(args[2])
}
if start+length > len(inner) {
length = len(inner) - start
}
return inner[start : start+length]
}
case "STR":
args := splitArgs(argsStr)
inner := a.evalKeyExprInner(args[0])
if len(args) >= 2 {
width := parseIntIdx(args[1])
s := string(inner)
return []byte(fmt.Sprintf("%*s", width, strings.TrimSpace(s)))
}
return inner
case "DTOS":
inner := a.evalKeyExprInner(argsStr)
// Date → YYYYMMDD sortable string
return inner
case "PADL":
args := splitArgs(argsStr)
if len(args) >= 2 {
inner := string(a.evalKeyExprInner(args[0]))
width := parseIntIdx(args[1])
fill := " "
if len(args) >= 3 {
fill = strings.Trim(args[2], "\"' ")
if fill == "" {
fill = " "
}
}
for len(inner) < width {
inner = fill + inner
}
return []byte(inner[:width])
}
case "PADR":
args := splitArgs(argsStr)
if len(args) >= 2 {
inner := string(a.evalKeyExprInner(args[0]))
width := parseIntIdx(args[1])
for len(inner) < width {
inner = inner + " "
}
return []byte(inner[:width])
}
default:
// Unknown function — use VM MacroEval for UDF calls
if KeyEvalFunc != nil {
fullExpr := expr[:parenOpen] + "(" + argsStr + ")"
val := KeyEvalFunc(fullExpr)
return valueToKeyBytes(val)
}
// Fallback: evaluate inner as field
return a.evalKeyExprInner(argsStr)
}
}
// Concatenation: expr1 + expr2 (find + not inside parens)
if plus := findOperator(expr, '+'); plus > 0 {
left := a.evalKeyExprInner(expr[:plus])
right := a.evalKeyExprInner(expr[plus+1:])
return append(left, right...)
}
// Numeric literal
s := strings.TrimSpace(expr)
if len(s) > 0 && (s[0] >= '0' && s[0] <= '9') {
return []byte(s)
}
// Final fallback: use VM MacroEval for any unresolvable expression
if KeyEvalFunc != nil {
val := KeyEvalFunc(expr)
return valueToKeyBytes(val)
}
return []byte(expr)
}
// evalForExpr evaluates a FOR condition for a given record. Returns true if record matches.
// Supports: FIELD = "value", FIELD = value, FIELD > value, !DELETED(), .T., .F.
func (a *DBFArea) evalForExpr(forExpr string, recNo uint32) bool {
oldRecNo := a.recNo
a.GoTo(recNo)
result := a.evalForInner(strings.TrimSpace(forExpr))
a.GoTo(oldRecNo)
return result
}
func (a *DBFArea) evalForInner(expr string) bool {
upper := strings.ToUpper(strings.TrimSpace(expr))
if upper == ".T." || upper == "TRUE" {
return true
}
if upper == ".F." || upper == "FALSE" {
return false
}
if upper == "!DELETED()" || upper == ".NOT. DELETED()" {
return !a.Deleted()
}
if upper == "DELETED()" {
return a.Deleted()
}
// FIELD = "value" or FIELD = value
for _, op := range []string{"==", "=", "!=", "<>", ">=", "<=", ">", "<"} {
if idx := strings.Index(expr, op); idx > 0 {
leftExpr := strings.TrimSpace(expr[:idx])
rightExpr := strings.TrimSpace(expr[idx+len(op):])
leftVal := string(a.evalKeyExprInner(leftExpr))
rightVal := strings.Trim(rightExpr, "\"' ")
leftTrim := strings.TrimRight(leftVal, " ")
switch op {
case "=", "==":
return leftTrim == rightVal || leftVal == rightVal
case "!=", "<>":
return leftTrim != rightVal && leftVal != rightVal
case ">":
return leftTrim > rightVal
case "<":
return leftTrim < rightVal
case ">=":
return leftTrim >= rightVal
case "<=":
return leftTrim <= rightVal
}
}
}
// .AND. / .OR.
if idx := strings.Index(upper, ".AND."); idx > 0 {
left := a.evalForInner(expr[:idx])
right := a.evalForInner(expr[idx+5:])
return left && right
}
if idx := strings.Index(upper, ".OR."); idx > 0 {
left := a.evalForInner(expr[:idx])
right := a.evalForInner(expr[idx+4:])
return left || right
}
return true // default: include record
}
// valueToKeyBytes converts a hbrt.Value to index key bytes.
func valueToKeyBytes(v hbrt.Value) []byte {
switch {
case v.IsString():
return []byte(v.AsString())
case v.IsNumeric():
return []byte(fmt.Sprintf("%20.10f", v.AsNumDouble()))
case v.IsDate(), v.IsTimestamp():
y, m, d := julianToDate(v.AsJulian())
return []byte(fmt.Sprintf("%04d%02d%02d", y, m, d))
case v.IsLogical():
if v.AsBool() {
return []byte("T")
}
return []byte("F")
default:
return []byte("")
}
}
// Helper: find matching close parenthesis
func findMatchingParen(s string, openPos int) int {
depth := 1
for i := openPos + 1; i < len(s); i++ {
if s[i] == '(' {
depth++
} else if s[i] == ')' {
depth--
if depth == 0 {
return i
}
}
}
return -1
}
// Helper: find operator not inside parentheses
func findOperator(s string, op byte) int {
depth := 0
for i := len(s) - 1; i > 0; i-- {
if s[i] == ')' {
depth++
} else if s[i] == '(' {
depth--
} else if s[i] == op && depth == 0 {
return i
}
}
return -1
}
// Helper: split comma-separated args respecting parentheses
func splitArgs(s string) []string {
var args []string
depth := 0
start := 0
for i := 0; i < len(s); i++ {
if s[i] == '(' {
depth++
} else if s[i] == ')' {
depth--
} else if s[i] == ',' && depth == 0 {
args = append(args, strings.TrimSpace(s[start:i]))
start = i + 1
}
}
args = append(args, strings.TrimSpace(s[start:]))
return args
}
func parseIntIdx(s string) int {
s = strings.TrimSpace(s)
n := 0
for _, c := range s {
if c >= '0' && c <= '9' {
n = n*10 + int(c-'0')
}
}
return n
}
// formatKeyValue converts a Value to index key bytes.
func formatKeyValue(val hbrt.Value, fi hbrdd.FieldInfo) []byte {
switch fi.Type {
case 'C':
s := val.AsString()
// Pad to field length
for len(s) < fi.Len {
s += " "
}
return []byte(s[:fi.Len])
case 'N':
s := fmt.Sprintf("%*.*f", fi.Len, fi.Dec, val.AsNumDouble())
return []byte(s)
case 'D':
return []byte(val.AsString())
default:
return []byte(val.AsString())
}
}