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>
2158 lines
48 KiB
Go
2158 lines
48 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"
|
||
"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("")
|
||
}
|