diff --git a/compiler/analyzer/analyzer.go b/compiler/analyzer/analyzer.go index e3aa417..1a6edeb 100644 --- a/compiler/analyzer/analyzer.go +++ b/compiler/analyzer/analyzer.go @@ -547,7 +547,7 @@ var rtlFunctions = map[string]bool{ "DBSEEK": true, "DBSELECTAREA": true, "DBPACK": true, "DBZAP": true, "DBCREATE": true, "DBINFO": true, "DBORDERINFO": true, "DBSETINDEX": true, // FiveSql2 hybrid hot-path RTL (pcode + Go-native scan) - "PCCOMPILE": true, "PCEVAL": true, "SQLSCAN": true, + "PCCOMPILE": true, "PCEVAL": true, "SQLSCAN": true, "SQLEACH": true, // Field metadata + index creation "FIELDTYPE": true, "FIELDLEN": true, "FIELDDEC": true, "ORDCREATE": true, "DBCREATEINDEX": true, "DBCLEARINDEX": true, diff --git a/hbrtl/register.go b/hbrtl/register.go index 086395e..cf3a76e 100644 --- a/hbrtl/register.go +++ b/hbrtl/register.go @@ -618,6 +618,7 @@ func RegisterRTL(vm *hbrt.VM) { hbrt.Sym("PCEVAL", hbrt.FsPublic, PcEval), // Go-native SQL scan loop (bypasses PRG interpreter for hot path) hbrt.Sym("SQLSCAN", hbrt.FsPublic, SqlScan), + hbrt.Sym("SQLEACH", hbrt.FsPublic, SqlEach), // Goroutine / Concurrency hbrt.Sym("GO", hbrt.FsPublic, GoFunc), diff --git a/hbrtl/sqlscan.go b/hbrtl/sqlscan.go index 2d8e71d..b6a9314 100644 --- a/hbrtl/sqlscan.go +++ b/hbrtl/sqlscan.go @@ -237,3 +237,146 @@ func SqlScan(t *hbrt.Thread) { t.PushValue(hbrt.MakeArrayFrom(rows)) t.RetValue() } + +// SqlEach(aFieldPositions, pcWhere, bBlock) → NIL +// +// Streaming variant of SqlScan — instead of materializing all matching +// rows into a result array (which costs N HbArray allocations plus a +// second pass when the PRG caller iterates it), we invoke a user-provided +// code block once per matching row, passing the selected field values as +// block parameters. +// +// This is the Harbour block-iteration idiom (`AEval`, `AScan`) applied +// to SQL. Total heap traffic collapses to ~0 — no result rows, no slab, +// no flat value buffer. Per-row overhead becomes just (field reads + +// WHERE eval + block invoke). +// +// Expected to hit raw-RDD parity on end-to-end "SQL → user code" timing. +// +// Arguments: +// aFieldPositions: 1-based field positions to pass as block params +// pcWhere: compiled WHERE predicate, or NIL +// bBlock: code block receiving nFields positional params +func SqlEach(t *hbrt.Thread) { + t.Frame(3, 0) + defer t.EndProc() + + fieldsVal := t.Local(1) + if !fieldsVal.IsArray() { + t.RetNil() + return + } + fieldsArr := fieldsVal.AsArray().Items + nFields := len(fieldsArr) + + whereVal := t.Local(2) + var whereFn *hbrt.PcodeFunc + if !whereVal.IsNil() { + if p := whereVal.AsPointer(); p != nil { + whereFn, _ = p.(*hbrt.PcodeFunc) + } + } + + blockVal := t.Local(3) + if !blockVal.IsBlock() { + t.RetNil() + return + } + blk := blockVal.AsBlock() + + fieldPos := make([]int, nFields) + for i := 0; i < nFields; i++ { + fieldPos[i] = int(fieldsArr[i].AsNumInt()) + if fieldPos[i] < 1 { + fieldPos[i] = 1 + } + } + + wam, ok := t.WA.(*hbrdd.WorkAreaManager) + if !ok { + t.RetNil() + return + } + area := wam.Current() + if area == nil { + t.RetNil() + return + } + dbfArea, _ := area.(*dbf.DBFArea) + + // Install FastFieldGetter for the WHERE predicate's PcOpFieldGet ops + prevFG := t.FastFieldGetter + if dbfArea != nil { + t.FastFieldGetter = func(idx int) hbrt.Value { + v, _ := dbfArea.GetValue(idx - 1) + return v + } + } else { + t.FastFieldGetter = func(idx int) hbrt.Value { + v, _ := area.GetValue(idx - 1) + return v + } + } + defer func() { t.FastFieldGetter = prevFG }() + + // Block eval protocol: push N args on the stack, set pendingParams, + // call blk.Fn(t). Matches what EvalBlock does inline, skipping the + // per-call `make([]Value, nArgs)` temp slice. + // + // Four specialized loops on {DBF, generic}×{WHERE, none}, same + // reasoning as SqlScan's loop split. + switch { + case dbfArea != nil && whereFn != nil: + dbfArea.GoTop() + for !dbfArea.EOF() { + hbrt.ExecPcodeFast(t, whereFn, nil) + if t.GetRetValue().AsBool() { + for i := 0; i < nFields; i++ { + v, _ := dbfArea.GetValue(fieldPos[i] - 1) + t.PushValue(v) + } + t.PendingParams2(nFields) + blk.Fn(t) + } + dbfArea.Skip(1) + } + case dbfArea != nil: + dbfArea.GoTop() + for !dbfArea.EOF() { + for i := 0; i < nFields; i++ { + v, _ := dbfArea.GetValue(fieldPos[i] - 1) + t.PushValue(v) + } + t.PendingParams2(nFields) + blk.Fn(t) + dbfArea.Skip(1) + } + case whereFn != nil: + area.GoTop() + for !area.EOF() { + hbrt.ExecPcodeFast(t, whereFn, nil) + if t.GetRetValue().AsBool() { + for i := 0; i < nFields; i++ { + v, _ := area.GetValue(fieldPos[i] - 1) + t.PushValue(v) + } + t.PendingParams2(nFields) + blk.Fn(t) + } + area.Skip(1) + } + default: + area.GoTop() + for !area.EOF() { + for i := 0; i < nFields; i++ { + v, _ := area.GetValue(fieldPos[i] - 1) + t.PushValue(v) + } + t.PendingParams2(nFields) + blk.Fn(t) + area.Skip(1) + } + } + + t.RetNil() +}