// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // Database callable functions: FIELDPUT, ALIAS, DBEVAL, DBUSEAREA, DBCLOSEAREA, // DBGOTO, DBSKIP, DBAPPEND, DBDELETE, DBRECALL, DBCOMMIT, DBSEEK, // DBGOTOP, DBGOBOTTOM, DBRLOCKLIST, DBSETFILTER, DBCLEARFILTER package hbrtl import ( "fmt" "os" "sort" "strings" "sync/atomic" "five/hbrt" "five/hbrdd" "five/hbrdd/dbf" ) // FIELDPUT(nField, xValue) → xValue func rtlFieldPut(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area == nil { t.RetNil() return } nField := t.Local(1).AsInt() val := t.Local(2) area.PutValue(nField-1, val) // 1-based to 0-based t.RetVal(val) } // ALIAS([nWorkArea]) → cAlias func rtlAlias(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetString("") return } area := wam.Current() if area != nil { t.RetString(area.Alias()) } else { t.RetString("") } } // DBEVAL(bBlock [, bFor [, bWhile [, nCount [, nRecord [, lRest]]]]]) → NIL func rtlDbEval(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area == nil { t.RetNil() return } block := t.Local(1) if !block.IsBlock() { t.RetNil() return } var bFor, bWhile hbrt.Value nCount := -1 lRest := false if nParams >= 2 { bFor = t.Local(2) } if nParams >= 3 { bWhile = t.Local(3) } if nParams >= 4 && !t.Local(4).IsNil() { nCount = t.Local(4).AsInt() } if nParams >= 5 && !t.Local(5).IsNil() { nRec := t.Local(5).AsInt() area.GoTo(uint32(nRec)) } if nParams >= 6 && !t.Local(6).IsNil() { lRest = t.Local(6).AsBool() } // If not lRest and no record specified, go top if !lRest && (nParams < 5 || t.Local(5).IsNil()) { area.GoTop() } count := 0 for !area.EOF() { if nCount >= 0 && count >= nCount { break } // While condition if !bWhile.IsNil() && bWhile.IsBlock() { blk := bWhile.AsBlock() t.PendingParams2(0) blk.Fn(t) if !t.GetRetValue().AsBool() { break } } // For condition doBlock := true if !bFor.IsNil() && bFor.IsBlock() { blk := bFor.AsBlock() t.PendingParams2(0) blk.Fn(t) doBlock = t.GetRetValue().AsBool() } if doBlock { blk := block.AsBlock() t.PendingParams2(0) blk.Fn(t) } area.Skip(1) count++ } t.RetNil() } // DBUSEAREA([lNewArea], [cDriver], cName, [cAlias], [lShared], [lReadOnly]) → NIL func rtlDbUseArea(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetNil() return } cName := "" cAlias := "" cDriver := "DBFNTX" if nParams >= 3 && !t.Local(3).IsNil() { cName = t.Local(3).AsString() } if nParams >= 4 && !t.Local(4).IsNil() { cAlias = t.Local(4).AsString() } if nParams >= 2 && !t.Local(2).IsNil() { cDriver = t.Local(2).AsString() } shared := false readOnly := false if nParams >= 5 && !t.Local(5).IsNil() { shared = t.Local(5).AsBool() } if nParams >= 6 && !t.Local(6).IsNil() { readOnly = t.Local(6).AsBool() } _, err := wam.Open(cDriver, cName, cAlias, shared, readOnly) if err != nil { panic(&hbrt.HbError{ Description: err.Error(), Operation: "DBUSEAREA", SubSystem: "BASE", }) } t.RetNil() } // DBCLOSEALL() → NIL — closes all open work areas func rtlDbCloseAll(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProcFast() wam := getWA(t) if wam != nil { wam.CloseAll() } t.RetNil() } // DBCLOSEAREA() → NIL func rtlDbCloseArea(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProcFast() wam := getWA(t) if wam != nil { wam.Close() } t.RetNil() } // DBGOTO(nRecNo) → NIL func rtlDbGoTo(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area != nil { area.GoTo(uint32(t.Local(1).AsLong())) } t.RetNil() } // DBSKIP([nRecords]) → NIL func rtlDbSkip(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area == nil { t.RetNil() return } n := int64(1) if nParams >= 1 && !t.Local(1).IsNil() { n = t.Local(1).AsLong() } area.Skip(n) t.RetNil() } // DBGOTOP() → NIL func rtlDbGoTop(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area != nil { area.GoTop() } t.RetNil() } // DBGOBOTTOM() → NIL func rtlDbGoBottom(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area != nil { area.GoBottom() } t.RetNil() } // DBAPPEND([lUnlock]) → NIL func rtlDbAppend(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area != nil { area.Append() } t.RetNil() } // DBDELETE() → NIL func rtlDbDelete(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area != nil { area.Delete() } t.RetNil() } // DBRECALL() → NIL func rtlDbRecall(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area != nil { area.Recall() } t.RetNil() } // DBRLOCK([nRecNo]) → lSuccess // Acquires an advisory byte-range lock on a single record via fcntl(F_SETLK). // Non-blocking: returns .F. if another process already holds the lock. // Harbour: SELF_LOCK(a, &lockInfo) with DBLM_EXCLUSIVE. func rtlDbRLock(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetBool(false) return } area := wam.Current() if area == nil { t.RetBool(false) return } da, ok := area.(*dbf.DBFArea) if !ok { // Non-DBF drivers: assume success (in-memory, etc.) t.RetBool(true) return } var recNo uint32 if nParams >= 1 && !t.Local(1).IsNil() { recNo = uint32(t.Local(1).AsNumInt()) } locked, err := da.LockRecord(recNo) if err != nil { t.RetBool(false) return } t.RetBool(locked) } // DBRUNLOCK([nRecNo]) → NIL // Releases a specific record lock, or all record locks held by this // workarea if called without arguments. Harbour: SELF_UNLOCK. func rtlDbRUnlock(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area == nil { t.RetNil() return } da, ok := area.(*dbf.DBFArea) if !ok { t.RetNil() return } var recNo uint32 if nParams >= 1 && !t.Local(1).IsNil() { recNo = uint32(t.Local(1).AsNumInt()) } _ = da.UnlockRecord(recNo) t.RetNil() } // FLOCK() → lSuccess // Acquires an exclusive file-wide lock via fcntl. Non-blocking: returns .F. // if another process holds the file lock. Harbour: SELF_LOCK with DBLM_FILE. func rtlFLock(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetBool(false) return } area := wam.Current() if area == nil { t.RetBool(false) return } da, ok := area.(*dbf.DBFArea) if !ok { t.RetBool(true) // in-memory etc. return } locked, err := da.LockFile() if err != nil { t.RetBool(false) return } t.RetBool(locked) } // DBUNLOCK() → NIL // Releases all locks (file + record) held by the current workarea. // Harbour: SELF_UNLOCK with no record number. func rtlDbUnlock(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area == nil { t.RetNil() return } if da, ok := area.(*dbf.DBFArea); ok { _ = da.UnlockRecord(0) _ = da.UnlockFile() } t.RetNil() } // DBCOMMIT() → NIL func rtlDbCommit(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area != nil { area.Flush() } t.RetNil() } // DBSEEK(xValue [, lSoftSeek [, lLast]]) → lFound func rtlDbSeek(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetBool(false) return } area := wam.Current() if area == nil { t.RetBool(false) return } val := t.Local(1) softSeek := GetSetSoftSeek() // default: check SET SOFTSEEK findLast := false if nParams >= 2 && !t.Local(2).IsNil() { softSeek = t.Local(2).AsBool() } if nParams >= 3 && !t.Local(3).IsNil() { findLast = t.Local(3).AsBool() } // Check if area implements Indexer if idx, ok := area.(hbrdd.Indexer); ok { found, _ := idx.Seek(val, softSeek, findLast) t.RetBool(found) } else { t.RetBool(false) } } // DBSELECTAREA(nArea | cAlias) → NIL func rtlDbSelectArea(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetNil() return } v := t.Local(1) if v.IsString() { wam.Select(v.AsString()) } else { wam.Select(uint16(v.AsInt())) } t.RetNil() } // DBPACK() → NIL func rtlDbPack(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area != nil { area.Pack() } t.RetNil() } // DBZAP() → NIL func rtlDbZap(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area != nil { area.Zap() } t.RetNil() } // --- LOCATE / CONTINUE --- // Harbour: DBLOCATE(bFor, bWhile, nNext, nRec, lRest) → .T./.F. // Searches from current position for record matching condition. func rtlDbLocate(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetBool(false) return } area := wam.Current() if area == nil { t.RetBool(false) return } // param 1: bFor condition block var bFor hbrt.Value if nParams >= 1 && !t.Local(1).IsNil() { bFor = t.Local(1) } // param 2: bWhile condition block var bWhile hbrt.Value if nParams >= 2 && !t.Local(2).IsNil() { bWhile = t.Local(2) } // param 3: nNext — max records to scan nNext := int64(0) // 0 = all if nParams >= 3 && !t.Local(3).IsNil() { nNext = t.Local(3).AsNumInt() } // param 4: nRec — specific record number if nParams >= 4 && !t.Local(4).IsNil() { nRec := uint32(t.Local(4).AsNumInt()) area.GoTo(nRec) if bFor.IsBlock() { t.PendingParams2(0) bFor.AsBlock().Fn(t) result := t.Pop2() area.SetFound(result.AsBool()) } else { area.SetFound(true) } t.RetBool(area.Found()) return } // param 5: lRest — .T. = continue from current, .F. = from top lRest := false if nParams >= 5 && !t.Local(5).IsNil() { lRest = t.Local(5).AsBool() } if !lRest && nNext == 0 { area.GoTop() } // Store locate block for __dbContinue if bFor.IsBlock() { area.SetLocate("", func(lt *hbrt.Thread) bool { lt.PendingParams2(0) bFor.AsBlock().Fn(lt) return lt.Pop2().AsBool() }) } found := false count := int64(0) for !area.EOF() { if nNext > 0 && count >= nNext { break } // Check WHILE condition if bWhile.IsBlock() { t.PendingParams2(0) bWhile.AsBlock().Fn(t) if !t.Pop2().AsBool() { break } } // Check FOR condition if bFor.IsBlock() { t.PendingParams2(0) bFor.AsBlock().Fn(t) if t.Pop2().AsBool() { found = true break } } else { found = true break } area.Skip(1) count++ } area.SetFound(found) t.RetBool(found) } // __DBCONTINUE — continue previous LOCATE search. // Harbour: __dbContinue() func rtlDbContinue(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetBool(false) return } area := wam.Current() if area == nil { t.RetBool(false) return } blk := area.LocateBlock() if blk == nil { area.SetFound(false) t.RetBool(false) return } // Skip past current record area.Skip(1) found := false for !area.EOF() { if blk(t) { found = true break } area.Skip(1) } area.SetFound(found) t.RetBool(found) } // rtlDbAverage implements __dbAverage(bExpr, bFor, bWhile, nNext, nRec, // lRest) — sum the expression over visible records and return the // arithmetic mean. Returns 0 when the loop visits no records (mirrors // Harbour's idiom of avoiding a divide-by-zero in the expansion). // // Used by `AVERAGE TO ` in std.ch. func rtlDbAverage(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetDouble(0, 10, 2) return } area := wam.Current() if area == nil { t.RetDouble(0, 10, 2) return } bExpr := t.Local(1) if !bExpr.IsBlock() { t.RetDouble(0, 10, 2) return } var bFor, bWhile hbrt.Value if nParams >= 2 { bFor = t.Local(2) } if nParams >= 3 { bWhile = t.Local(3) } nCount := -1 if nParams >= 4 && !t.Local(4).IsNil() { nCount = t.Local(4).AsInt() } if nParams >= 5 && !t.Local(5).IsNil() { area.GoTo(uint32(t.Local(5).AsInt())) } lRest := false if nParams >= 6 && !t.Local(6).IsNil() { lRest = t.Local(6).AsBool() } if !lRest && (nParams < 5 || t.Local(5).IsNil()) { area.GoTop() } sum := 0.0 n := 0 scanned := 0 for !area.EOF() { if nCount >= 0 && scanned >= nCount { break } // WHILE if bWhile.IsBlock() { t.PendingParams2(0) bWhile.AsBlock().Fn(t) if !t.GetRetValue().AsBool() { break } } // FOR eval := true if bFor.IsBlock() { t.PendingParams2(0) bFor.AsBlock().Fn(t) eval = t.GetRetValue().AsBool() } if eval { t.PendingParams2(0) bExpr.AsBlock().Fn(t) sum += t.GetRetValue().AsNumDouble() n++ } area.Skip(1) scanned++ } if n == 0 { t.RetDouble(0, 10, 2) return } t.RetDouble(sum/float64(n), 10, 2) } // dbCmdTmpSeq generates a unique numeric suffix for temp aliases // used by COPY/SORT/TOTAL/JOIN. Without this, a nested call (e.g., // COPY inside a FOR clause that itself runs COPY) would collide on // the same `__copytmp` alias and the inner Open would fail with // "alias already in use". Atomic so concurrent goroutines (each // owns its own WorkAreaManager but the counter is process-wide) // don't hand out duplicates. var dbCmdTmpSeq uint64 func nextTmpAlias(prefix string) string { n := atomic.AddUint64(&dbCmdTmpSeq, 1) return fmt.Sprintf("%s_%d", prefix, n) } // dbHashKey turns a workarea field value into a hash-table key // string. Numeric / Date / Logical / String types are encoded with a // distinct one-byte tag prefix so values that happen to share the // same string form across types ("1" vs 1 vs .T.) don't collide. // NIL is its own bucket — Harbour's `==` says NIL never equals // anything, but for join semantics we treat NIL keys as a single // bucket so the user can still JOIN over rows with missing keys // when both sides are NIL. func dbHashKey(v hbrt.Value) string { switch { case v.IsNil(): return "\x00" case v.IsNumeric(): return fmt.Sprintf("N\x01%g", v.AsNumDouble()) case v.IsLogical(): if v.AsBool() { return "L\x01T" } return "L\x01F" case v.IsDate(): return fmt.Sprintf("D\x01%d", v.AsJulian()) case v.IsString(): return "S\x01" + strings.TrimRight(v.AsString(), " ") } return "?\x01" + v.AsString() } // rtlDbNotImpl raises a runtime error explaining which xBase clause // the user invoked that Five doesn't yet implement. std.ch routes // SDF / DELIMITED / TO PRINTER / TO FILE variants here so they fail // loudly with a helpful diagnostic instead of being silently dropped. func rtlDbNotImpl(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() what := "" if nParams >= 1 && t.Local(1).IsString() { what = t.Local(1).AsString() } panic(&hbrt.HbError{ Description: "xBase clause not implemented: " + what, SubSystem: "BASE", }) } // rtlDbCopy implements __dbCopy(cFile, aFields, bFor, bWhile, nNext, // xRec, lRest) — copy visible records from the current workarea into a // freshly created DBF. Field projection: an empty/missing aFields // copies the whole structure; otherwise only fields whose names match // (case-insensitive) are carried over. Used by `COPY TO [FIELDS] // [FOR] [WHILE] [NEXT] [RECORD] [REST] [ALL]` in std.ch. // // Harbour's __dbCopy also accepts cRDD / nConnection / cCodepage / xDelim // (params 8..11). Five only supports DBFNTX→DBFNTX for now; SDF/DELIMITED // copies stay parser no-ops until that backend lands. func rtlDbCopy(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetBool(false) return } srcArea := wam.Current() if srcArea == nil { t.RetBool(false) return } if nParams < 1 || t.Local(1).IsNil() { t.RetBool(false) return } cFile := t.Local(1).AsString() if cFile == "" { t.RetBool(false) return } // Field projection. Harbour passes `{ <(fields)> }` so each entry // is a string literal already; uppercase for case-insensitive // matching against the source's field names. var srcIdx []int var dstFields []hbrdd.FieldInfo nSrcFields := srcArea.FieldCount() useAll := true if nParams >= 2 && t.Local(2).IsArray() { arr := t.Local(2).AsArray() if arr != nil && len(arr.Items) > 0 { useAll = false wanted := make(map[string]struct{}, len(arr.Items)) for _, it := range arr.Items { s := strings.ToUpper(strings.TrimSpace(it.AsString())) if s != "" { wanted[s] = struct{}{} } } for i := 0; i < nSrcFields; i++ { fi := srcArea.GetFieldInfo(i) if _, ok := wanted[strings.ToUpper(fi.Name)]; ok { srcIdx = append(srcIdx, i) dstFields = append(dstFields, fi) } } } } if useAll { srcIdx = make([]int, nSrcFields) dstFields = make([]hbrdd.FieldInfo, nSrcFields) for i := 0; i < nSrcFields; i++ { srcIdx[i] = i dstFields[i] = srcArea.GetFieldInfo(i) } } if len(dstFields) == 0 { // Nothing to copy — empty FIELDS list with no matches. t.RetBool(false) return } // Loop bounds — same shape as dbEval. var bFor, bWhile hbrt.Value if nParams >= 3 { bFor = t.Local(3) } if nParams >= 4 { bWhile = t.Local(4) } nCount := -1 if nParams >= 5 && !t.Local(5).IsNil() { nCount = t.Local(5).AsInt() } if nParams >= 6 && !t.Local(6).IsNil() { srcArea.GoTo(uint32(t.Local(6).AsInt())) } lRest := false if nParams >= 7 && !t.Local(7).IsNil() { lRest = t.Local(7).AsBool() } if !lRest && (nParams < 6 || t.Local(6).IsNil()) { srcArea.GoTop() } // Create + open the destination. Use a temp alias so we don't // clash with whatever the caller may have open under a name // matching the file's basename. drv, err := hbrdd.GetDriver("DBFNTX") if err != nil { t.RetBool(false) return } if _, err := drv.Create(hbrdd.CreateParams{Path: cFile, Fields: dstFields}); err != nil { t.RetBool(false) return } srcSel := wam.CurrentNum() dstSel, err := wam.Open("DBFNTX", cFile, nextTmpAlias("__copytmp"), false, false) if err != nil { // drv.Create wrote the file but Open failed (alias collision, // area-table full, ...). Without cleanup the user is left // with a stale-zero-row DBF on disk that the next call will // silently truncate again. _ = os.Remove(cFile) t.RetBool(false) return } dstArea := wam.AreaAt(dstSel) wam.SelectByNum(srcSel) scanned := 0 for !srcArea.EOF() { if nCount >= 0 && scanned >= nCount { break } if bWhile.IsBlock() { t.PendingParams2(0) bWhile.AsBlock().Fn(t) if !t.GetRetValue().AsBool() { break } } emit := true if bFor.IsBlock() { t.PendingParams2(0) bFor.AsBlock().Fn(t) emit = t.GetRetValue().AsBool() } if emit { vals := make([]hbrt.Value, len(srcIdx)) for i, idx := range srcIdx { v, _ := srcArea.GetValue(idx) vals[i] = v } wam.SelectByNum(dstSel) dstArea.Append() for i, v := range vals { dstArea.PutValue(i, v) } wam.SelectByNum(srcSel) } srcArea.Skip(1) scanned++ } // Close the destination, leaving the source selected as on entry. // A close error means the just-written DBF may be partially // flushed — surface it so the caller doesn't assume the file is // durable and proceed to delete the source. wam.SelectByNum(dstSel) closeErr := wam.Close() wam.SelectByNum(srcSel) if closeErr != nil { t.RetBool(false) return } t.RetBool(true) } // rtlDbSort implements __dbSort(cFile, aFields, bFor, bWhile, nNext, // xRec, lRest) — same loop semantics as __dbCopy but the visible // records are buffered, sorted by the named keys, and written in // order. Each entry of aFields may be a plain field name (ascending) // or `name/D` for descending. Unrecognized suffixes are ignored. // // Used by `SORT TO [ON ] [FOR/WHILE/NEXT/...]` in // std.ch. func rtlDbSort(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetBool(false) return } srcArea := wam.Current() if srcArea == nil { t.RetBool(false) return } if nParams < 1 || t.Local(1).IsNil() { t.RetBool(false) return } cFile := t.Local(1).AsString() if cFile == "" { t.RetBool(false) return } nSrcFields := srcArea.FieldCount() dstFields := make([]hbrdd.FieldInfo, nSrcFields) for i := 0; i < nSrcFields; i++ { dstFields[i] = srcArea.GetFieldInfo(i) } if nSrcFields == 0 { t.RetBool(false) return } // Parse sort-key spec: name[/D|/A|/C[/D]|/D[/C]] entries. // /D = descending, /A = ascending (default), /C = case-insensitive. // /C and /D can be combined — `name/CD` or `name/DC` both ok. type sortKey struct { idx int desc bool caseFold bool } var keys []sortKey if nParams >= 2 && t.Local(2).IsArray() { for _, item := range t.Local(2).AsArray().Items { s := strings.TrimSpace(item.AsString()) if s == "" { continue } desc := false caseFold := false // Strip suffixes — may be multiple, e.g. `name/C/D` or // `name/CD`. Walk left from the last `/` repeatedly. for { i := strings.LastIndexByte(s, '/') if i < 0 { break } suffix := strings.ToUpper(strings.TrimSpace(s[i+1:])) if suffix == "" { break } done := false for _, ch := range suffix { switch ch { case 'D': desc = true case 'A': // ascending — explicit no-op case 'C': caseFold = true default: // Unknown letter — leave the suffix attached // to the name and stop parsing so a field // like `name/foo` doesn't get silently mangled. done = true } if done { break } } if done { break } s = strings.TrimSpace(s[:i]) } upper := strings.ToUpper(s) idx := -1 for k := 0; k < nSrcFields; k++ { if strings.EqualFold(dstFields[k].Name, upper) { idx = k break } } if idx >= 0 { keys = append(keys, sortKey{idx: idx, desc: desc, caseFold: caseFold}) } } } // Loop bounds. var bFor, bWhile hbrt.Value if nParams >= 3 { bFor = t.Local(3) } if nParams >= 4 { bWhile = t.Local(4) } nCount := -1 if nParams >= 5 && !t.Local(5).IsNil() { nCount = t.Local(5).AsInt() } if nParams >= 6 && !t.Local(6).IsNil() { srcArea.GoTo(uint32(t.Local(6).AsInt())) } lRest := false if nParams >= 7 && !t.Local(7).IsNil() { lRest = t.Local(7).AsBool() } if !lRest && (nParams < 6 || t.Local(6).IsNil()) { srcArea.GoTop() } // Buffer visible records' field values. var rows [][]hbrt.Value scanned := 0 for !srcArea.EOF() { if nCount >= 0 && scanned >= nCount { break } if bWhile.IsBlock() { t.PendingParams2(0) bWhile.AsBlock().Fn(t) if !t.GetRetValue().AsBool() { break } } emit := true if bFor.IsBlock() { t.PendingParams2(0) bFor.AsBlock().Fn(t) emit = t.GetRetValue().AsBool() } if emit { row := make([]hbrt.Value, nSrcFields) for i := 0; i < nSrcFields; i++ { v, _ := srcArea.GetValue(i) row[i] = v } rows = append(rows, row) } srcArea.Skip(1) scanned++ } // Sort if any keys were given. Stable so equal keys keep input // order. Comparison is type-aware: numeric by AsNumDouble, date by // AsNumInt julian, logical by truth, otherwise string. /C keys // fold case before string comparison. if len(keys) > 0 && len(rows) > 1 { less := func(i, j int) bool { for _, k := range keys { a := rows[i][k.idx] b := rows[j][k.idx] var cmp int if k.caseFold && a.IsString() && b.IsString() { sa := strings.ToLower(a.AsString()) sb := strings.ToLower(b.AsString()) switch { case sa < sb: cmp = -1 case sa > sb: cmp = 1 } } else { cmp = compareValues(a, b) } if cmp == 0 { continue } if k.desc { cmp = -cmp } return cmp < 0 } return false } stableSort(rows, less) } // Create + open destination, then append in order. drv, err := hbrdd.GetDriver("DBFNTX") if err != nil { t.RetBool(false) return } if _, err := drv.Create(hbrdd.CreateParams{Path: cFile, Fields: dstFields}); err != nil { t.RetBool(false) return } srcSel := wam.CurrentNum() dstSel, err := wam.Open("DBFNTX", cFile, nextTmpAlias("__sorttmp"), false, false) if err != nil { _ = os.Remove(cFile) t.RetBool(false) return } dstArea := wam.AreaAt(dstSel) for _, row := range rows { dstArea.Append() for i, v := range row { dstArea.PutValue(i, v) } } wam.SelectByNum(dstSel) closeErr := wam.Close() wam.SelectByNum(srcSel) if closeErr != nil { t.RetBool(false) return } t.RetBool(true) } // rtlDbList implements __dbList(lOff, aBlocks, lAll, bFor, bWhile, // nNext, nRec, lRest, cFile) — output visible records to stdout, or // to the named file when cFile is non-empty. aBlocks is an array of // column-evaluation code blocks (one per LIST / DISPLAY column // expression). If aBlocks is empty or contains only NIL placeholders, // every field of the current workarea is emitted. // // Used by both `LIST []` and `DISPLAY []` in std.ch. // lAll distinguishes them: LIST always passes .T. (all matching // records); DISPLAY passes .T. only for `DISPLAY ALL`, otherwise .F. // (just the current record). // // TO FILE <(f)> redirects output into a freshly-truncated text file // (one record per line, fields space-separated). TO PRINTER is // rejected at PP-time via __dbNotImpl — Five doesn't drive a // printer port. OFF (lOff) suppresses the record-number prefix. func rtlDbList(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetNil() return } srcArea := wam.Current() if srcArea == nil { t.RetNil() return } lOff := false if nParams >= 1 && !t.Local(1).IsNil() { lOff = t.Local(1).AsBool() } // Decode column blocks. Empty / `{ NIL }` → fall back to "all fields". var blocks []hbrt.Value useAllFields := true if nParams >= 2 && t.Local(2).IsArray() { arr := t.Local(2).AsArray() if arr != nil { for _, it := range arr.Items { if it.IsBlock() { blocks = append(blocks, it) useAllFields = false } } } } lAll := true if nParams >= 3 && !t.Local(3).IsNil() { lAll = t.Local(3).AsBool() } // Loop bounds — same shape as dbEval. var bFor, bWhile hbrt.Value if nParams >= 4 { bFor = t.Local(4) } if nParams >= 5 { bWhile = t.Local(5) } nCount := -1 if nParams >= 6 && !t.Local(6).IsNil() { nCount = t.Local(6).AsInt() } if nParams >= 7 && !t.Local(7).IsNil() { srcArea.GoTo(uint32(t.Local(7).AsInt())) } lRest := false if nParams >= 8 && !t.Local(8).IsNil() { lRest = t.Local(8).AsBool() } // DISPLAY without ALL emits exactly one record; LIST always emits // the full filtered range. Encode the difference by clamping // nCount to 1 when lAll is false and no explicit NEXT was given. if !lAll && nCount < 0 { nCount = 1 } if !lRest && lAll && (nParams < 7 || t.Local(7).IsNil()) { srcArea.GoTop() } // param 9: cFile — when non-empty, redirect output into the named // text file. The previous file is truncated. We deliberately keep // the file open across the loop so the OS doesn't see N opens for // N rows; close on exit. On open failure: fall back to stdout // rather than producing partial output to nowhere. var sink interface { Write([]byte) (int, error) } = os.Stdout if nParams >= 9 && t.Local(9).IsString() { if cFile := strings.TrimSpace(t.Local(9).AsString()); cFile != "" { f, err := os.Create(cFile) if err != nil { panic(&hbrt.HbError{ Description: "LIST/DISPLAY TO FILE: cannot create " + cFile + " — " + err.Error(), SubSystem: "BASE", }) } defer f.Close() sink = f } } nFields := srcArea.FieldCount() scanned := 0 for !srcArea.EOF() { if nCount >= 0 && scanned >= nCount { break } if bWhile.IsBlock() { t.PendingParams2(0) bWhile.AsBlock().Fn(t) if !t.GetRetValue().AsBool() { break } } emit := true if bFor.IsBlock() { t.PendingParams2(0) bFor.AsBlock().Fn(t) emit = t.GetRetValue().AsBool() } if emit { parts := []string{} if !lOff { // Deleted records get a `*` after the recno when shown // (only happens with SET DELETED OFF — DELETED ON // already skipped them at Area.Skip level). Matches // Harbour LIST/DISPLAY convention. marker := " " if srcArea.Deleted() { marker = "*" } parts = append(parts, fmt.Sprintf("%6d%s", srcArea.RecNo(), marker)) } if useAllFields { for i := 0; i < nFields; i++ { v, _ := srcArea.GetValue(i) parts = append(parts, valueToDisplay(v)) } } else { for _, blk := range blocks { t.PendingParams2(0) blk.AsBlock().Fn(t) parts = append(parts, valueToDisplay(t.GetRetValue())) } } // Newline after the row, not before — avoids the spurious // leading blank line at the top of the listing. `\n` only; // terminals handle CR conversion themselves. Goes to the // chosen sink (stdout by default, the file when TO FILE // was used). fmt.Fprintln(sink, strings.Join(parts, " ")) } srcArea.Skip(1) scanned++ } t.RetNil() } // rtlDbTotal implements __dbTotal(cFile, bKey, aFields, bFor, bWhile, // nNext, xRec, lRest) — emit one record per *consecutive* run of // equal key values from the current workarea, summing the named // numeric fields and copying every other field from the run's first // record. The source must already be sorted/indexed on the key for // the grouping to produce one row per distinct value (Harbour's // dbtotal.prg has the same precondition). // // Used by `TOTAL TO ON [FIELDS ] [FOR/WHILE/...]` in // std.ch. func rtlDbTotal(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetBool(false) return } srcArea := wam.Current() if srcArea == nil { t.RetBool(false) return } if nParams < 1 || t.Local(1).IsNil() { t.RetBool(false) return } cFile := t.Local(1).AsString() if cFile == "" { t.RetBool(false) return } bKey := t.Local(2) if !bKey.IsBlock() { t.RetBool(false) return } // Build dst struct — drop memos like Harbour does. nFields := srcArea.FieldCount() var dstFields []hbrdd.FieldInfo var keptIdx []int for i := 0; i < nFields; i++ { fi := srcArea.GetFieldInfo(i) if fi.Type == 'M' { continue } dstFields = append(dstFields, fi) keptIdx = append(keptIdx, i) } if len(dstFields) == 0 { t.RetBool(false) return } // Resolve sum-fields → list of (dstIdx, srcIdx) pairs preserving // declaration order. type sumPair struct{ dst, src int } var sums []sumPair if nParams >= 3 && t.Local(3).IsArray() { arr := t.Local(3).AsArray() if arr != nil { wanted := map[string]struct{}{} for _, it := range arr.Items { s := strings.ToUpper(strings.TrimSpace(it.AsString())) if s != "" { wanted[s] = struct{}{} } } for di, si := range keptIdx { if _, ok := wanted[strings.ToUpper(dstFields[di].Name)]; ok { sums = append(sums, sumPair{dst: di, src: si}) } } } } // Loop bounds. var bFor, bWhile hbrt.Value if nParams >= 4 { bFor = t.Local(4) } if nParams >= 5 { bWhile = t.Local(5) } nCount := -1 if nParams >= 6 && !t.Local(6).IsNil() { nCount = t.Local(6).AsInt() } if nParams >= 7 && !t.Local(7).IsNil() { srcArea.GoTo(uint32(t.Local(7).AsInt())) } lRest := false if nParams >= 8 && !t.Local(8).IsNil() { lRest = t.Local(8).AsBool() } if !lRest && (nParams < 7 || t.Local(7).IsNil()) { srcArea.GoTop() } // Create + open destination. drv, err := hbrdd.GetDriver("DBFNTX") if err != nil { t.RetBool(false) return } if _, err := drv.Create(hbrdd.CreateParams{Path: cFile, Fields: dstFields}); err != nil { t.RetBool(false) return } srcSel := wam.CurrentNum() dstSel, err := wam.Open("DBFNTX", cFile, nextTmpAlias("__totaltmp"), false, false) if err != nil { _ = os.Remove(cFile) t.RetBool(false) return } dstArea := wam.AreaAt(dstSel) wam.SelectByNum(srcSel) // Group walk. var prevKey hbrt.Value haveGroup := false running := make([]float64, len(sums)) overflowed := false // Pre-compute the max-magnitude representable in each sum-field: // 10^(Len-Dec) - 10^(-Dec). Anything at or beyond this gets // formatted as `*****` by the DBF codec, so we surface the issue // instead of writing garbage. maxAbs := make([]float64, len(sums)) for i, sp := range sums { fi := dstFields[sp.dst] intDigits := fi.Len - fi.Dec if fi.Dec > 0 { intDigits-- } if intDigits < 0 { intDigits = 0 } m := 1.0 for d := 0; d < intDigits; d++ { m *= 10 } maxAbs[i] = m } flush := func() { if !haveGroup { return } wam.SelectByNum(dstSel) // The dst row for this group is the most recent append. // Overwrite the sum-field positions with the accumulated total, // preserving the field's declared length/decimals. for i, sp := range sums { fi := dstFields[sp.dst] v := running[i] abs := v if abs < 0 { abs = -abs } if v != v || abs >= maxAbs[i] { // NaN or out-of-range overflowed = true } dstArea.PutValue(sp.dst, hbrt.MakeDouble(v, uint16(fi.Len), uint16(fi.Dec))) } wam.SelectByNum(srcSel) for i := range running { running[i] = 0 } haveGroup = false } scanned := 0 for !srcArea.EOF() { if nCount >= 0 && scanned >= nCount { break } if bWhile.IsBlock() { t.PendingParams2(0) bWhile.AsBlock().Fn(t) if !t.GetRetValue().AsBool() { break } } match := true if bFor.IsBlock() { t.PendingParams2(0) bFor.AsBlock().Fn(t) match = t.GetRetValue().AsBool() } if match { t.PendingParams2(0) bKey.AsBlock().Fn(t) curKey := t.GetRetValue() if !haveGroup || compareValues(prevKey, curKey) != 0 { flush() // Append a fresh dst row and copy this first-of-group // record's non-memo fields into it. wam.SelectByNum(dstSel) dstArea.Append() wam.SelectByNum(srcSel) for di, si := range keptIdx { v, _ := srcArea.GetValue(si) wam.SelectByNum(dstSel) dstArea.PutValue(di, v) wam.SelectByNum(srcSel) } prevKey = curKey haveGroup = true } // Sum this record's contribution. for i, sp := range sums { v, _ := srcArea.GetValue(sp.src) running[i] += v.AsNumDouble() } } srcArea.Skip(1) scanned++ } flush() wam.SelectByNum(dstSel) closeErr := wam.Close() wam.SelectByNum(srcSel) if closeErr != nil { t.RetBool(false) return } if overflowed { // One or more group totals didn't fit in the destination // field's declared width. The DBF codec wrote `*****` for // those cells; flag the caller so they don't trust the file. t.RetBool(false) return } t.RetBool(true) } // rtlDbJoin implements __dbJoin(cAlias, cFile, aFields, bFor) — emit // the cartesian product of the current ("master") workarea and the // named "detail" workarea, filtered by bFor. Output structure: // // * No FIELDS clause: master's fields followed by detail's fields, // dropping detail-side names that clash with master. // * FIELDS list: in declaration order, each name is resolved // against master first then detail. // // Same shape as harbour-core/src/rdd/dbjoin.prg. Five-specific // simplifications: alias->name FIELD notation isn't supported yet // (bare names with master-precedence lookup); RDD/codepage args // dropped. func rtlDbJoin(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetBool(false) return } master := wam.Current() if master == nil { t.RetBool(false) return } masterSel := wam.CurrentNum() // param 1: detail workarea alias if nParams < 1 || t.Local(1).IsNil() { t.RetBool(false) return } cAlias := strings.TrimSpace(t.Local(1).AsString()) if cAlias == "" { t.RetBool(false) return } detailSel := wam.FindByAlias(cAlias) if detailSel == 0 { t.RetBool(false) return } detail := wam.AreaAt(detailSel) if detail == nil { t.RetBool(false) return } // param 2: destination file name if nParams < 2 || t.Local(2).IsNil() { t.RetBool(false) return } cFile := t.Local(2).AsString() if cFile == "" { t.RetBool(false) return } // Build dst struct + a per-dst-field source descriptor (which // area to read from at output time). type srcRef struct { isMaster bool idx int } var dstFields []hbrdd.FieldInfo var srcRefs []srcRef addField := func(fi hbrdd.FieldInfo, isMaster bool, srcIdx int) { // Skip if name already present (master wins). for _, e := range dstFields { if strings.EqualFold(e.Name, fi.Name) { return } } dstFields = append(dstFields, fi) srcRefs = append(srcRefs, srcRef{isMaster: isMaster, idx: srcIdx}) } // FIELDS list — empty means union of all fields. var wanted []string if nParams >= 3 && t.Local(3).IsArray() { arr := t.Local(3).AsArray() if arr != nil { for _, it := range arr.Items { s := strings.TrimSpace(it.AsString()) if s != "" { wanted = append(wanted, strings.ToUpper(s)) } } } } if len(wanted) == 0 { // All master fields, then detail fields with master-name precedence. for i := 0; i < master.FieldCount(); i++ { addField(master.GetFieldInfo(i), true, i) } for i := 0; i < detail.FieldCount(); i++ { addField(detail.GetFieldInfo(i), false, i) } } else { // User-specified order: master first, then detail. for _, n := range wanted { found := false for i := 0; i < master.FieldCount(); i++ { fi := master.GetFieldInfo(i) if strings.EqualFold(fi.Name, n) { addField(fi, true, i) found = true break } } if !found { for i := 0; i < detail.FieldCount(); i++ { fi := detail.GetFieldInfo(i) if strings.EqualFold(fi.Name, n) { addField(fi, false, i) break } } } } } if len(dstFields) == 0 { t.RetBool(false) return } // param 4: FOR block. Empty std.ch rule wraps it as `{|| .T. }`, // so a missing block here means "always true". Treat NIL-block // the same way for direct callers. bFor := hbrt.Value{} if nParams >= 4 { bFor = t.Local(4) } // Create + open destination. drv, err := hbrdd.GetDriver("DBFNTX") if err != nil { t.RetBool(false) return } if _, err := drv.Create(hbrdd.CreateParams{Path: cFile, Fields: dstFields}); err != nil { t.RetBool(false) return } dstSel, err := wam.Open("DBFNTX", cFile, nextTmpAlias("__jointmp"), false, false) if err != nil { _ = os.Remove(cFile) t.RetBool(false) return } dstArea := wam.AreaAt(dstSel) // Hash-join fast path. When the caller passes master and detail // key field names (params 5 + 6), build a hash table over the // detail in O(M), then scan master in O(N) and probe — total // O(N+M) instead of the nested-loop's O(N*M). For 1k×1k that's // 2k vs 1M operations, and the gap widens fast. masterKeyName := "" detailKeyName := "" if nParams >= 5 && t.Local(5).IsString() { masterKeyName = strings.ToUpper(strings.TrimSpace(t.Local(5).AsString())) } if nParams >= 6 && t.Local(6).IsString() { detailKeyName = strings.ToUpper(strings.TrimSpace(t.Local(6).AsString())) } if masterKeyName != "" && detailKeyName != "" { mkIdx, dkIdx := -1, -1 for i := 0; i < master.FieldCount(); i++ { if strings.EqualFold(master.GetFieldInfo(i).Name, masterKeyName) { mkIdx = i break } } for i := 0; i < detail.FieldCount(); i++ { if strings.EqualFold(detail.GetFieldInfo(i).Name, detailKeyName) { dkIdx = i break } } if mkIdx >= 0 && dkIdx >= 0 { // Build detail hash: key string → list of cached field rows. // We capture each detail row's wanted-field VALUES (not just // rec numbers) so we don't have to re-select the detail area // per probe — saves the WA-switch round trip and keeps the // inner loop tight. type detailRow struct { vals []hbrt.Value } buckets := make(map[string][]detailRow, 1024) wam.SelectByNum(detailSel) detail.GoTop() for !detail.EOF() { k, _ := detail.GetValue(dkIdx) key := dbHashKey(k) row := detailRow{vals: make([]hbrt.Value, 0, len(srcRefs))} for _, r := range srcRefs { if r.isMaster { row.vals = append(row.vals, hbrt.MakeNil()) // master fills later } else { v, _ := detail.GetValue(r.idx) row.vals = append(row.vals, v) } } buckets[key] = append(buckets[key], row) detail.Skip(1) } // Scan master, probe detail buckets. wam.SelectByNum(masterSel) master.GoTop() for !master.EOF() { mk, _ := master.GetValue(mkIdx) key := dbHashKey(mk) rows, hit := buckets[key] if hit { // Cache master-side values for this row once. mvals := make([]hbrt.Value, len(srcRefs)) for k, r := range srcRefs { if r.isMaster { v, _ := master.GetValue(r.idx) mvals[k] = v } } wam.SelectByNum(dstSel) for _, drow := range rows { dstArea.Append() for k, r := range srcRefs { if r.isMaster { dstArea.PutValue(k, mvals[k]) } else { dstArea.PutValue(k, drow.vals[k]) } } } wam.SelectByNum(masterSel) } master.Skip(1) } goto closeDst } // Key names didn't resolve — fall through to nested-loop with // (likely empty) bFor. User typo is reported as a no-result // JOIN rather than crash; the destination DBF still gets // created (matches Harbour: NO ROWS != error). } wam.SelectByNum(masterSel) master.GoTop() for !master.EOF() { wam.SelectByNum(detailSel) detail.GoTop() for !detail.EOF() { wam.SelectByNum(masterSel) match := true if bFor.IsBlock() { t.PendingParams2(0) bFor.AsBlock().Fn(t) match = t.GetRetValue().AsBool() } if match { vals := make([]hbrt.Value, len(srcRefs)) for k, r := range srcRefs { if r.isMaster { v, _ := master.GetValue(r.idx) vals[k] = v } else { v, _ := detail.GetValue(r.idx) vals[k] = v } } wam.SelectByNum(dstSel) dstArea.Append() for k, v := range vals { dstArea.PutValue(k, v) } wam.SelectByNum(masterSel) } wam.SelectByNum(detailSel) detail.Skip(1) } wam.SelectByNum(masterSel) master.Skip(1) } closeDst: wam.SelectByNum(dstSel) closeErr := wam.Close() wam.SelectByNum(masterSel) if closeErr != nil { t.RetBool(false) return } t.RetBool(true) } // rtlDbUpdate implements __dbUpdate(cAlias, bKey, lRandom, bAssign) — // for each record in the detail workarea, find the matching master // record (by key equality) and apply bAssign in master's context. // Same shape as harbour-core/src/rdd/dbupdat.prg: // // * bKey runs in either context — typically a bare field name that // exists in both areas. // * bAssign runs in *master* context — Harbour's std.ch wraps each // ` WITH ` clause as `_FIELD-> := ` so the assignment // targets the master row while `` is free to read detail->... // * lRandom .T.: scan master from top for each detail key. Otherwise // both areas must be sorted on the key; the master pointer just // walks forward through equal-or-greater keys (matches Harbour's // forward-walk semantics for the non-random branch). // // Used by `UPDATE FROM [ON ] [RANDOM] REPLACE ...` // in std.ch. func rtlDbUpdate(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetBool(false) return } master := wam.Current() if master == nil { t.RetBool(false) return } masterSel := wam.CurrentNum() if nParams < 1 || t.Local(1).IsNil() { t.RetBool(false) return } cAlias := strings.TrimSpace(t.Local(1).AsString()) if cAlias == "" { t.RetBool(false) return } detailSel := wam.FindByAlias(cAlias) if detailSel == 0 { t.RetBool(false) return } detail := wam.AreaAt(detailSel) if detail == nil { t.RetBool(false) return } bKey := hbrt.Value{} if nParams >= 2 { bKey = t.Local(2) } if !bKey.IsBlock() { // No key block — nothing to drive the update by. t.RetBool(false) return } lRandom := false if nParams >= 3 && !t.Local(3).IsNil() { lRandom = t.Local(3).AsBool() } bAssign := hbrt.Value{} if nParams >= 4 { bAssign = t.Local(4) } if !bAssign.IsBlock() { t.RetBool(false) return } // Position both areas at top. master.GoTop() wam.SelectByNum(detailSel) detail.GoTop() wam.SelectByNum(masterSel) for { wam.SelectByNum(detailSel) if detail.EOF() { break } // Key from the detail row. t.PendingParams2(0) bKey.AsBlock().Fn(t) detailKey := t.GetRetValue() wam.SelectByNum(masterSel) if lRandom { // Linear scan from top — index-aware seek would be // faster but Five's seek API isn't part of the Area // interface yet. master.GoTop() for !master.EOF() { t.PendingParams2(0) bKey.AsBlock().Fn(t) if compareValues(t.GetRetValue(), detailKey) == 0 { break } master.Skip(1) } } else { // Walk forward while master's key < detail's key. for !master.EOF() { t.PendingParams2(0) bKey.AsBlock().Fn(t) if compareValues(t.GetRetValue(), detailKey) >= 0 { break } master.Skip(1) } } if !master.EOF() { // Match check (also covers the random branch's // "found something" outcome). t.PendingParams2(0) bKey.AsBlock().Fn(t) if compareValues(t.GetRetValue(), detailKey) == 0 { t.PendingParams2(0) bAssign.AsBlock().Fn(t) } } wam.SelectByNum(detailSel) detail.Skip(1) } wam.SelectByNum(masterSel) t.RetBool(true) } // stableSort sorts rows in place via the stdlib's `sort.SliceStable` // — O(n log n) with stable ordering preserved for equal keys. The // previous insertion-sort implementation degraded to O(n²) and was // unusable for DBFs over a few thousand rows. func stableSort(rows [][]hbrt.Value, less func(i, j int) bool) { sort.SliceStable(rows, less) } // --- DBSETFILTER / DBCLEARFILTER / DBFILTER --- // DBSETFILTER(bCondition [, cCondition]) func rtlDbSetFilter(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area == nil { t.RetNil() return } if nParams >= 1 && t.Local(1).IsBlock() { bFilter := t.Local(1) expr := "" if nParams >= 2 && t.Local(2).IsString() { expr = t.Local(2).AsString() } area.SetFilter(expr, func(lt *hbrt.Thread) bool { lt.PendingParams2(0) bFilter.AsBlock().Fn(lt) return lt.Pop2().AsBool() }) } t.RetNil() } // DBCLEARFILTER() func rtlDbClearFilter(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProcFast() wam := getWA(t) if wam == nil { t.RetNil() return } if area := wam.Current(); area != nil { area.ClearFilter() } t.RetNil() } // DBFILTER() → cFilterExpression func rtlDbFilter(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProcFast() // TODO: return stored filter expression from area t.RetString("") }