Files
five/hbrtl/database.go
CharlesKWON 2008266da7 feat(pp,rtl): Tier 2 audit followups — JOIN hash + PP validation + C heuristic
Three medium-priority audit items in one commit, each independently
revertible.

  * **#18 JOIN hash-join fast path.** New std.ch shape:
        JOIN WITH <alias> TO <file> [FIELDS ...] ON <mfield> = <dfield>
    expands to a 6-arg __dbJoin call with the master/detail key
    field names. Runtime detects the extra args, builds an O(M)
    hash over the detail's key column, then probes per master row
    for O(N+M) total — vs the FOR form's O(N*M). For 1k×1k that's
    2k vs 1M operations; the gap widens with N. The original FOR
    form is unchanged and stays the fallback for arbitrary
    predicates. New helper dbHashKey type-tags the key string so
    `1` (numeric), `"1"` (string), and `.T.` (logical) don't
    collide in the bucket map.

  * **#38 PP rule result-marker validation.** ParseRule now walks
    the result template after parseMarkers and warns about every
    `<name>` (or `<(name)>` / `<.name.>` / `<{name}>` / `#<name>`
    / `<"name">`) that doesn't match a pattern marker. Warnings
    flow into pp.errors via handleDirective with the directive's
    filename:line, so a typo'd `<NaMe>` in an `#xcommand`
    case-sensitive rule fails the build with a clear diagnostic
    instead of silently producing broken expansions.

  * **#44 looksLikeInlineC heuristic strengthened.** Catches more
    of the common Harbour-PRG-with-C-inline-block shapes that
    used to fall through and produce cryptic Go-side errors:
    function-like #define, `extern "C"` linkage blocks, C return-
    type declarations (`int foo(`, `static char* bar(`), and the
    hb_ret*() helper family used by Harbour's C FFI return
    setters. Two small predicate helpers (allLetters,
    allIdentChars) keep the C-vs-Go disambiguation tight enough
    that legit Go code (`func name() int { ... }`) doesn't trip.

  * **#28 LIST/DISPLAY pagination** — explicitly deferred. Proper
    pagination requires interactive terminal handling (Inkey(0)
    for the keypress) which would hang in CI / batch mode. Will
    revisit when an interactive terminal layer needs it for
    other reasons.

Test fixtures: tests/std_ch/test_join_hash.prg verifies the new
ON-form path produces the same output as the FOR form would.
std.ch runner now stands at 16/16.

Other gates green:
  go test ./...      : PASS
  FiveSql2 SQL:1999  : 43/43
  Harbour compat     : 56/56
  std.ch suite       : 16/16
  FRB suite          : 7/7

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

2158 lines
48 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 <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)
}
// 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 := "<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, 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 <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, 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 [<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 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 <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, 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
// `<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 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("")
}