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:
@@ -482,8 +482,11 @@ func (g *Generator) emitStmt(stmt ast.Stmt, locals localMap) {
|
|||||||
if s.ForCond != nil {
|
if s.ForCond != nil {
|
||||||
forExpr = fmt.Sprintf("%q", exprToString(s.ForCond))
|
forExpr = fmt.Sprintf("%q", exprToString(s.ForCond))
|
||||||
}
|
}
|
||||||
|
// Set VM callback for UDF evaluation during index build
|
||||||
|
g.writeln("dbf.KeyEvalFunc = func(expr string) hbrt.Value { return t.MacroEval(expr) }")
|
||||||
g.writeln(fmt.Sprintf("idx.OrderCreate(hbrdd.OrderCreateParams{KeyExpr: _keyExpr, FilePath: _file, ForExpr: %s, Unique: %v, Descending: %v})",
|
g.writeln(fmt.Sprintf("idx.OrderCreate(hbrdd.OrderCreateParams{KeyExpr: _keyExpr, FilePath: _file, ForExpr: %s, Unique: %v, Descending: %v})",
|
||||||
forExpr, s.Unique, s.Descending))
|
forExpr, s.Unique, s.Descending))
|
||||||
|
g.writeln("dbf.KeyEvalFunc = nil")
|
||||||
g.indent--
|
g.indent--
|
||||||
g.writeln("}")
|
g.writeln("}")
|
||||||
g.indent--
|
g.indent--
|
||||||
|
|||||||
55
examples/test_index_udf.prg
Normal file
55
examples/test_index_udf.prg
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// Test INDEX ON with User Defined Functions
|
||||||
|
PROCEDURE Main()
|
||||||
|
LOCAL i, aStruct, aCit, nCidx
|
||||||
|
|
||||||
|
aStruct := {{"ID","N",6,0}, {"NAME","C",20,0}, {"CITY","C",15,0}}
|
||||||
|
dbCreate("udf_test", aStruct)
|
||||||
|
USE "udf_test" NEW
|
||||||
|
|
||||||
|
aCit := {"Seoul","Tokyo","Beijing"}
|
||||||
|
FOR i := 1 TO 10
|
||||||
|
APPEND BLANK
|
||||||
|
REPLACE ID WITH i
|
||||||
|
REPLACE NAME WITH PadR("Name_" + PadL(LTrim(Str(i)), 3, "0"), 20)
|
||||||
|
nCidx := Int((i-1) % 3) + 1
|
||||||
|
REPLACE CITY WITH PadR(aCit[nCidx], 15)
|
||||||
|
NEXT
|
||||||
|
? "Records:", RecCount()
|
||||||
|
|
||||||
|
// Test 1: INDEX ON with built-in function (baseline)
|
||||||
|
INDEX ON UPPER(NAME) TO udf_idx1
|
||||||
|
GO TOP
|
||||||
|
? "T1 UPPER top:", RTrim(FieldGet(2)), RecNo()
|
||||||
|
SEEK PadR("NAME_005", 20)
|
||||||
|
? "T2 UPPER seek:", Found(), RecNo()
|
||||||
|
|
||||||
|
// Test 2: INDEX ON with UDF
|
||||||
|
INDEX ON MyKey(FIELD->NAME) TO udf_idx2
|
||||||
|
GO TOP
|
||||||
|
? "T3 UDF top:", RTrim(FieldGet(2)), RecNo()
|
||||||
|
SEEK PadR("KEY:Name_001", 20)
|
||||||
|
? "T4 UDF seek:", Found(), RecNo()
|
||||||
|
|
||||||
|
// Test 3: INDEX ON with two-param UDF
|
||||||
|
INDEX ON CityKey(FIELD->CITY, FIELD->NAME) TO udf_idx3
|
||||||
|
GO TOP
|
||||||
|
? "T5 UDF2 top:", RTrim(FieldGet(3)), RTrim(FieldGet(2)), RecNo()
|
||||||
|
|
||||||
|
// Test 4: Nested: built-in wrapping UDF
|
||||||
|
INDEX ON Left(MyKey(FIELD->NAME), 15) TO udf_idx4
|
||||||
|
GO TOP
|
||||||
|
? "T6 Nested top:", RTrim(FieldGet(2)), RecNo()
|
||||||
|
|
||||||
|
CLOSE ALL
|
||||||
|
? "DONE"
|
||||||
|
RETURN
|
||||||
|
|
||||||
|
FUNCTION MyKey(cName)
|
||||||
|
LOCAL cResult
|
||||||
|
cResult := "KEY:" + RTrim(cName)
|
||||||
|
RETURN PadR(cResult, 20)
|
||||||
|
|
||||||
|
FUNCTION CityKey(cCity, cName)
|
||||||
|
LOCAL cResult
|
||||||
|
cResult := RTrim(cCity) + ":" + RTrim(cName)
|
||||||
|
RETURN PadR(cResult, 30)
|
||||||
@@ -46,6 +46,12 @@ type indexState struct {
|
|||||||
scopeBottom []byte // bottom 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.
|
// ensureIndexState initializes the index state if nil.
|
||||||
func (a *DBFArea) ensureIndexState() {
|
func (a *DBFArea) ensureIndexState() {
|
||||||
if a.idxState == nil {
|
if a.idxState == nil {
|
||||||
@@ -848,9 +854,16 @@ func (a *DBFArea) evalKeyExprInner(expr string) []byte {
|
|||||||
}
|
}
|
||||||
return []byte(inner[:width])
|
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)
|
// Concatenation: expr1 + expr2 (find + not inside parens)
|
||||||
@@ -866,6 +879,12 @@ func (a *DBFArea) evalKeyExprInner(expr string) []byte {
|
|||||||
return []byte(s)
|
return []byte(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Final fallback: use VM MacroEval for any unresolvable expression
|
||||||
|
if KeyEvalFunc != nil {
|
||||||
|
val := KeyEvalFunc(expr)
|
||||||
|
return valueToKeyBytes(val)
|
||||||
|
}
|
||||||
|
|
||||||
return []byte(expr)
|
return []byte(expr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -937,6 +956,26 @@ func (a *DBFArea) evalForInner(expr string) bool {
|
|||||||
return true // default: include record
|
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
|
// Helper: find matching close parenthesis
|
||||||
func findMatchingParen(s string, openPos int) int {
|
func findMatchingParen(s string, openPos int) int {
|
||||||
depth := 1
|
depth := 1
|
||||||
|
|||||||
Reference in New Issue
Block a user