// 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 ( "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) } // --- 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("") }