perf: FieldPos O(1) cache + xbase import detection for function-call PRGs

Two SQLite-style optimizations for RDD and SQL workloads:

1. FieldPos() O(1) column binding cache

   Before: FieldPos(name) linear scan — O(n) per call with string
           comparison. In SQL engines that call FieldPos per row per
           column, this is hundreds of thousands of calls.

   After:  DBFArea builds a map[UPPER(name)]→pos on first lookup.
           All subsequent lookups are O(1) hash. SQLite calls this
           "column affinity binding" — positions resolved at prepare,
           not per row.

   Implementation:
     - hbrdd/dbf/dbf.go: DBFArea.FieldPosCache(name) method
     - hbrtl/procinfo.go: FieldPos RTL uses fieldPosCacher interface
     - Lazy init: only pays for tables that get queried

2. hbrdd import auto-detection for function-call style PRGs

   Before: compiler only added hbrdd import when PRG used xBase commands
           (USE, SKIP, INDEX...). Pure function-call style like
           `dbUseArea(.T.,,"t")`, `FieldPut(1, val)` was missed —
           generated Go failed to compile ("undefined: hbrdd").

   After:  scanStmtsForXBase walks ExprStmt bodies too, detecting
           CallExpr to any of the ~40 xBase RTL function names.
           FIELD->NAME alias expressions also trigger the import.

   Resolves: small PRGs that use only dbUseArea/FieldGet/FieldPut.

Benchmark notes (50k records):
  Raw RDD scan:              7 ms    (baseline)
  FiveSql2 SELECT WHERE:   157 ms    (unchanged — bottleneck is
                                      not FieldPos, it's PRG-level
                                      expression tree walk per row)
  compat_harbour 51/51:    PASS
  FiveSql2 43/43:          100%

The FieldPos cache helps heavy field-name-based code paths but the
primary FiveSql2 bottleneck is the PRG interpreter walking expression
ASTs per row (needs bytecode compilation to close the gap).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 07:42:00 +09:00
parent 7cc729f394
commit ed33af41c5
3 changed files with 98 additions and 6 deletions

View File

@@ -9,6 +9,7 @@ import (
"five/hbrt"
"os"
"strconv"
"strings"
"time"
)
@@ -109,8 +110,8 @@ func Center(t *hbrt.Thread) {
// FIELDPOS(cFieldName) → nPos
func FieldPos(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProc()
fname := t.Local(1).AsString()
defer t.EndProcFast()
fname := strings.ToUpper(t.Local(1).AsString())
wam := getWA(t)
if wam == nil {
t.RetInt(0)
@@ -121,10 +122,23 @@ func FieldPos(t *hbrt.Thread) {
t.RetInt(0)
return
}
// Try DBFArea's built-in field position cache (O(1) hash lookup).
// Falls back to linear scan for non-DBF areas (mem RDD, etc.).
type fieldPosCacher interface {
FieldPosCache(name string) int
}
if fpc, ok := area.(fieldPosCacher); ok {
pos := fpc.FieldPosCache(fname)
t.RetInt(int64(pos))
return
}
// Fallback: linear scan
for i := 0; i < area.FieldCount(); i++ {
fi := area.GetFieldInfo(i)
if eqFold(fi.Name, fname) {
t.RetInt(int64(i + 1)) // Harbour: 1-based position
if strings.EqualFold(fi.Name, fname) {
t.RetInt(int64(i + 1))
return
}
}