// 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" "strings" "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) } // 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, "__copytmp", false, false) if err != nil { 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. wam.SelectByNum(dstSel) wam.Close() wam.SelectByNum(srcSel) 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] entries → (fieldIdx, descending). type sortKey struct { idx int desc 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 // Suffix `/D` (descending), `/A` (ascending), `/C` // (case-insensitive — treated as ascending). if i := strings.LastIndexByte(s, '/'); i > 0 { suffix := strings.ToUpper(strings.TrimSpace(s[i+1:])) switch suffix { case "D": desc = true } 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}) } } } // 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. 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] 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, "__sorttmp", false, false) if err != nil { t.RetBool(false) return } dstArea := wam.AreaAt(dstSel) for _, row := range rows { dstArea.Append() for i, v := range row { dstArea.PutValue(i, v) } } wam.Close() wam.SelectByNum(srcSel) t.RetBool(true) } // rtlDbList implements __dbList(lOff, aBlocks, lAll, bFor, bWhile, // nNext, nRec, lRest, lPrn, cFile) — output visible records to // stdout. 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 PRINTER / TO FILE redirection (lPrn / cFile) is accepted but // not yet implemented — both paths still write to stdout. 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() } 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 { parts = append(parts, fmt.Sprintf("%6d", srcArea.RecNo())) } 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())) } } fmt.Print("\r\n" + 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, "__totaltmp", false, false) if err != nil { t.RetBool(false) return } dstArea := wam.AreaAt(dstSel) wam.SelectByNum(srcSel) // Group walk. var prevKey hbrt.Value haveGroup := false running := make([]float64, len(sums)) 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] dstArea.PutValue(sp.dst, hbrt.MakeDouble(running[i], 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) wam.Close() wam.SelectByNum(srcSel) t.RetBool(true) } // stableSort is a tiny insertion sort for small N (typical DBF SORT // targets are interactive datasets). Avoids a sort import dependency. func stableSort(rows [][]hbrt.Value, less func(i, j int) bool) { for i := 1; i < len(rows); i++ { for j := i; j > 0 && less(j, j-1); j-- { rows[j], rows[j-1] = rows[j-1], rows[j] } } } // --- 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("") }