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

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