Files
five/hbrtl/database.go
CharlesKWON 5b1d3fb32f feat(pp,rtl): pre-release accuracy round (Wave 3)
Four audit findings around correctness/consistency in std.ch and the
SORT/UPDATE/TOTAL handlers:

  * #13: TOTAL/UPDATE key idiom inconsistency documented as inherent.
    TOTAL evaluates `<key>` only in the source workarea so verbatim
    `<{key}>` (alias-qualified or `_FIELD->`-prefixed by the user)
    works. UPDATE evaluates the same block in BOTH master and detail
    context, so it must wrap as `_FIELD-><key>` to dispatch to
    whichever WA is selected at eval time. The two rules look alike
    but their evaluation contexts differ — also documented in
    std.ch alongside both rules so the asymmetry isn't a surprise.
    Plus: TOTAL TO and ON are now mandatory (matching the COUNT/
    UPDATE pattern from Wave 1) — bare TOTAL would have produced
    broken syntax via the unconditional `<(f)>`/`<{key}>` template
    references.

  * #15/#16: SDF / DELIMITED variants of COPY and TO PRINTER /
    TO FILE variants of LIST / DISPLAY are now matched by stub
    rules (placed *before* the regular rules so they win) that
    expand to a new `__dbNotImpl(reason)` RTL primitive raising a
    clear `&hbrt.HbError`. BEGIN SEQUENCE / RECOVER catches the
    panic, so callers get a real error instead of the previous
    silent dispatch-to-regular-DBF-copy.

  * #19: SORT /C (case-insensitive) now actually folds case before
    the string compare, instead of being silently treated as
    ascending. Suffix parser also rebuilt as a multi-letter scanner
    so `name/CD`, `name/DC`, `name/C/D`, `name/D/C` all parse the
    same way — combine /C and /D freely. Unknown suffix letters
    (e.g., `name/X`) leave the suffix attached to the field name
    so a stray slash in user input doesn't get silently mangled
    into a broken field reference.

  * #27 SET DELETED: verified with a regression test that
    `SET DELETED ON` causes COUNT/COPY (and by extension
    SORT/TOTAL/JOIN/UPDATE — all of which iterate via Area.Skip)
    to skip rows marked deleted. The filtering is implemented at
    the workarea level (skipFilter in dbf.go honors hbrdd.IsSetDeleted)
    so no RTL changes were needed; this commit just adds the
    coverage so the behavior doesn't silently regress.

Gates green:
  go test ./...      : PASS
  FiveSql2 SQL:1999  : 43/43
  Harbour compat     : 56/56

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 08:01:42 +09:00

1985 lines
42 KiB
Go

// 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"
"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 <x> TO <v>` 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)
}
// 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 := "<unspecified>"
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 <f> [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 {
// 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 <f> [ON <field-list>] [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, "__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, 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 [<v,...>]` and `DISPLAY [<v,...>]` 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 <f> ON <key> [FIELDS <list>] [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 {
_ = 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, "__jointmp", false, false)
if err != nil {
_ = os.Remove(cFile)
t.RetBool(false)
return
}
dstArea := wam.AreaAt(dstSel)
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)
}
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
// `<f> WITH <x>` clause as `_FIELD-><f> := <x>` so the assignment
// targets the master row while `<x>` 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 <alias> [ON <key>] [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 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("")
}