feat: INDEX ON with UDF support — user functions in key expressions

Core change:
- dbf.KeyEvalFunc: global callback set by gengo before OrderCreate
- evalKeyExprInner default case: calls KeyEvalFunc for unknown functions
- Final fallback: any unresolvable expression → KeyEvalFunc → MacroEval
- valueToKeyBytes: converts MacroEval result to index key bytes
- gengo: sets dbf.KeyEvalFunc = t.MacroEval before OrderCreate, clears after

Examples that now work:
  INDEX ON MyFunc(FIELD->NAME) TO idx    // UDF in key expression
  INDEX ON CityKey(FIELD->CITY, NAME) TO idx  // multi-param UDF
  INDEX ON Left(MyFunc(NAME), 15) TO idx // nested built-in + UDF

Also fixed:
- SET ORDER TO n: int→string via hbrt.NtoS (was empty string)
- CDX compound leaf decoder: proper bit-packed tag name extraction
- CDX compound recNo = direct byte offset (not page number)

All existing tests pass, NTX 47/47 + CDX 20/20 Harbour compat maintained.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 13:36:21 +09:00
parent 7e2a159b88
commit c04c9aeaa8
3 changed files with 99 additions and 2 deletions

View File

@@ -46,6 +46,12 @@ type indexState struct {
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 {
@@ -848,9 +854,16 @@ func (a *DBFArea) evalKeyExprInner(expr string) []byte {
}
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)
}
// Unknown function — try to evaluate inner as field
return a.evalKeyExprInner(argsStr)
}
// Concatenation: expr1 + expr2 (find + not inside parens)
@@ -866,6 +879,12 @@ func (a *DBFArea) evalKeyExprInner(expr string) []byte {
return []byte(s)
}
// Final fallback: use VM MacroEval for any unresolvable expression
if KeyEvalFunc != nil {
val := KeyEvalFunc(expr)
return valueToKeyBytes(val)
}
return []byte(expr)
}
@@ -937,6 +956,26 @@ func (a *DBFArea) evalForInner(expr string) bool {
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