`LIST [<fields>] [OFF] [FOR ...] [WHILE ...] [NEXT ...] [RECORD ...]
[REST] [ALL]` and `DISPLAY [<fields>] [OFF] [FOR ...] ... [ALL]`
reach the parser as plain function calls to a new RTL primitive
__dbList (rtlDbList in hbrtl/database.go).
Implementation: walk the workarea under dbEval-style FOR/WHILE/NEXT/
RECORD/REST bounds. For each visible record, evaluate each column
block and emit the rendered values via valueToDisplay (the same
formatter QOut already uses). Empty fields list defaults to
"all fields". OFF suppresses the record-number prefix.
LIST always emits the full filtered range; DISPLAY without ALL emits
only the current record (encoded as nCount=1). TO PRINTER / TO FILE
clauses are not yet wired through — for now everything goes to
stdout.
Wiring up LIST/DISPLAY surfaced four further gaps in PP that were
silently masking bugs in any rule with multiple word-list / list /
optional clauses chained together:
* matchSegment refused MarkerWordList inside `[...]`. The LIST
rule's `[<off:OFF>]` clause therefore never set the off
capture, and `<.off.>` substituted to nothing instead of .T./.F.
matchSegment now matches WordList markers the same way the
top-level matcher does.
* `<v,...>` and `<(f)>` capture stop boundaries didn't include the
values of following MarkerWordList markers. For
`[<v,...>] [<off:OFF>] [<all:ALL>]` against `LIST id, name OFF`,
the v list would happily eat OFF. New addStopFrom helper
contributes both literal keywords and word-list values; both
matchSegment's MarkerList branch and captureExpression now use
it.
* Optional-repeat loop in matchPattern merged a no-progress
iteration's empty capture into the running multi-capture string
(with the `\x01` separator) before the no-progress break check
fired. So a successful first iteration's value got contaminated
and the substitution loop then skipped it as multi-capture
garbage. The merge now happens after the progress check.
* Unreferenced `<.name.>` markers (optional clauses that didn't
match in the input) were getting cleaned up to empty by the
generic marker scrubber instead of the .F. sentinel Harbour's
std.ch expects. New replaceUnreferencedLogify pass mirrors the
existing replaceUnreferencedBlockify and runs just before the
cleanup.
Parser cleanup: LIST and DISPLAY removed from the IDENT-statement
no-op switch in both parseIdentStmt and parseExprStmt.
Gates green:
go test ./... : PASS
FiveSql2 SQL:1999 : 43/43
Harbour compat : 56/56
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1318 lines
26 KiB
Go
1318 lines
26 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"
|
|
"strings"
|
|
|
|
"five/hbrt"
|
|
"five/hbrdd"
|
|
"five/hbrdd/dbf"
|
|
)
|
|
|
|
// FIELDPUT(nField, xValue) → xValue
|
|
func rtlFieldPut(t *hbrt.Thread) {
|
|
t.Frame(2, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
nField := t.Local(1).AsInt()
|
|
val := t.Local(2)
|
|
area.PutValue(nField-1, val) // 1-based to 0-based
|
|
t.RetVal(val)
|
|
}
|
|
|
|
// ALIAS([nWorkArea]) → cAlias
|
|
func rtlAlias(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetString("")
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
t.RetString(area.Alias())
|
|
} else {
|
|
t.RetString("")
|
|
}
|
|
}
|
|
|
|
// DBEVAL(bBlock [, bFor [, bWhile [, nCount [, nRecord [, lRest]]]]]) → NIL
|
|
func rtlDbEval(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
|
|
block := t.Local(1)
|
|
if !block.IsBlock() {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
|
|
var bFor, bWhile hbrt.Value
|
|
nCount := -1
|
|
lRest := false
|
|
|
|
if nParams >= 2 {
|
|
bFor = t.Local(2)
|
|
}
|
|
if nParams >= 3 {
|
|
bWhile = t.Local(3)
|
|
}
|
|
if nParams >= 4 && !t.Local(4).IsNil() {
|
|
nCount = t.Local(4).AsInt()
|
|
}
|
|
if nParams >= 5 && !t.Local(5).IsNil() {
|
|
nRec := t.Local(5).AsInt()
|
|
area.GoTo(uint32(nRec))
|
|
}
|
|
if nParams >= 6 && !t.Local(6).IsNil() {
|
|
lRest = t.Local(6).AsBool()
|
|
}
|
|
|
|
// If not lRest and no record specified, go top
|
|
if !lRest && (nParams < 5 || t.Local(5).IsNil()) {
|
|
area.GoTop()
|
|
}
|
|
|
|
count := 0
|
|
for !area.EOF() {
|
|
if nCount >= 0 && count >= nCount {
|
|
break
|
|
}
|
|
|
|
// While condition
|
|
if !bWhile.IsNil() && bWhile.IsBlock() {
|
|
blk := bWhile.AsBlock()
|
|
t.PendingParams2(0)
|
|
blk.Fn(t)
|
|
if !t.GetRetValue().AsBool() {
|
|
break
|
|
}
|
|
}
|
|
|
|
// For condition
|
|
doBlock := true
|
|
if !bFor.IsNil() && bFor.IsBlock() {
|
|
blk := bFor.AsBlock()
|
|
t.PendingParams2(0)
|
|
blk.Fn(t)
|
|
doBlock = t.GetRetValue().AsBool()
|
|
}
|
|
|
|
if doBlock {
|
|
blk := block.AsBlock()
|
|
t.PendingParams2(0)
|
|
blk.Fn(t)
|
|
}
|
|
|
|
area.Skip(1)
|
|
count++
|
|
}
|
|
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBUSEAREA([lNewArea], [cDriver], cName, [cAlias], [lShared], [lReadOnly]) → NIL
|
|
func rtlDbUseArea(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
cName := ""
|
|
cAlias := ""
|
|
cDriver := "DBFNTX"
|
|
if nParams >= 3 && !t.Local(3).IsNil() {
|
|
cName = t.Local(3).AsString()
|
|
}
|
|
if nParams >= 4 && !t.Local(4).IsNil() {
|
|
cAlias = t.Local(4).AsString()
|
|
}
|
|
if nParams >= 2 && !t.Local(2).IsNil() {
|
|
cDriver = t.Local(2).AsString()
|
|
}
|
|
shared := false
|
|
readOnly := false
|
|
if nParams >= 5 && !t.Local(5).IsNil() {
|
|
shared = t.Local(5).AsBool()
|
|
}
|
|
if nParams >= 6 && !t.Local(6).IsNil() {
|
|
readOnly = t.Local(6).AsBool()
|
|
}
|
|
_, err := wam.Open(cDriver, cName, cAlias, shared, readOnly)
|
|
if err != nil {
|
|
panic(&hbrt.HbError{
|
|
Description: err.Error(),
|
|
Operation: "DBUSEAREA",
|
|
SubSystem: "BASE",
|
|
})
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBCLOSEALL() → NIL — closes all open work areas
|
|
func rtlDbCloseAll(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam != nil {
|
|
wam.CloseAll()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBCLOSEAREA() → NIL
|
|
func rtlDbCloseArea(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam != nil {
|
|
wam.Close()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBGOTO(nRecNo) → NIL
|
|
func rtlDbGoTo(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
area.GoTo(uint32(t.Local(1).AsLong()))
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBSKIP([nRecords]) → NIL
|
|
func rtlDbSkip(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
n := int64(1)
|
|
if nParams >= 1 && !t.Local(1).IsNil() {
|
|
n = t.Local(1).AsLong()
|
|
}
|
|
area.Skip(n)
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBGOTOP() → NIL
|
|
func rtlDbGoTop(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
area.GoTop()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBGOBOTTOM() → NIL
|
|
func rtlDbGoBottom(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
area.GoBottom()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBAPPEND([lUnlock]) → NIL
|
|
func rtlDbAppend(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
area.Append()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBDELETE() → NIL
|
|
func rtlDbDelete(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
area.Delete()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBRECALL() → NIL
|
|
func rtlDbRecall(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
area.Recall()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBRLOCK([nRecNo]) → lSuccess
|
|
// Acquires an advisory byte-range lock on a single record via fcntl(F_SETLK).
|
|
// Non-blocking: returns .F. if another process already holds the lock.
|
|
// Harbour: SELF_LOCK(a, &lockInfo) with DBLM_EXCLUSIVE.
|
|
func rtlDbRLock(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
da, ok := area.(*dbf.DBFArea)
|
|
if !ok {
|
|
// Non-DBF drivers: assume success (in-memory, etc.)
|
|
t.RetBool(true)
|
|
return
|
|
}
|
|
var recNo uint32
|
|
if nParams >= 1 && !t.Local(1).IsNil() {
|
|
recNo = uint32(t.Local(1).AsNumInt())
|
|
}
|
|
locked, err := da.LockRecord(recNo)
|
|
if err != nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
t.RetBool(locked)
|
|
}
|
|
|
|
// DBRUNLOCK([nRecNo]) → NIL
|
|
// Releases a specific record lock, or all record locks held by this
|
|
// workarea if called without arguments. Harbour: SELF_UNLOCK.
|
|
func rtlDbRUnlock(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
da, ok := area.(*dbf.DBFArea)
|
|
if !ok {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
var recNo uint32
|
|
if nParams >= 1 && !t.Local(1).IsNil() {
|
|
recNo = uint32(t.Local(1).AsNumInt())
|
|
}
|
|
_ = da.UnlockRecord(recNo)
|
|
t.RetNil()
|
|
}
|
|
|
|
// FLOCK() → lSuccess
|
|
// Acquires an exclusive file-wide lock via fcntl. Non-blocking: returns .F.
|
|
// if another process holds the file lock. Harbour: SELF_LOCK with DBLM_FILE.
|
|
func rtlFLock(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
da, ok := area.(*dbf.DBFArea)
|
|
if !ok {
|
|
t.RetBool(true) // in-memory etc.
|
|
return
|
|
}
|
|
locked, err := da.LockFile()
|
|
if err != nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
t.RetBool(locked)
|
|
}
|
|
|
|
// DBUNLOCK() → NIL
|
|
// Releases all locks (file + record) held by the current workarea.
|
|
// Harbour: SELF_UNLOCK with no record number.
|
|
func rtlDbUnlock(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
if da, ok := area.(*dbf.DBFArea); ok {
|
|
_ = da.UnlockRecord(0)
|
|
_ = da.UnlockFile()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBCOMMIT() → NIL
|
|
func rtlDbCommit(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
area.Flush()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBSEEK(xValue [, lSoftSeek [, lLast]]) → lFound
|
|
func rtlDbSeek(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
val := t.Local(1)
|
|
softSeek := GetSetSoftSeek() // default: check SET SOFTSEEK
|
|
findLast := false
|
|
if nParams >= 2 && !t.Local(2).IsNil() {
|
|
softSeek = t.Local(2).AsBool()
|
|
}
|
|
if nParams >= 3 && !t.Local(3).IsNil() {
|
|
findLast = t.Local(3).AsBool()
|
|
}
|
|
// Check if area implements Indexer
|
|
if idx, ok := area.(hbrdd.Indexer); ok {
|
|
found, _ := idx.Seek(val, softSeek, findLast)
|
|
t.RetBool(found)
|
|
} else {
|
|
t.RetBool(false)
|
|
}
|
|
}
|
|
|
|
// DBSELECTAREA(nArea | cAlias) → NIL
|
|
func rtlDbSelectArea(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
v := t.Local(1)
|
|
if v.IsString() {
|
|
wam.Select(v.AsString())
|
|
} else {
|
|
wam.Select(uint16(v.AsInt()))
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBPACK() → NIL
|
|
func rtlDbPack(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
area.Pack()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBZAP() → NIL
|
|
func rtlDbZap(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area != nil {
|
|
area.Zap()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// --- LOCATE / CONTINUE ---
|
|
// Harbour: DBLOCATE(bFor, bWhile, nNext, nRec, lRest) → .T./.F.
|
|
// Searches from current position for record matching condition.
|
|
|
|
func rtlDbLocate(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
|
|
// param 1: bFor condition block
|
|
var bFor hbrt.Value
|
|
if nParams >= 1 && !t.Local(1).IsNil() {
|
|
bFor = t.Local(1)
|
|
}
|
|
// param 2: bWhile condition block
|
|
var bWhile hbrt.Value
|
|
if nParams >= 2 && !t.Local(2).IsNil() {
|
|
bWhile = t.Local(2)
|
|
}
|
|
// param 3: nNext — max records to scan
|
|
nNext := int64(0) // 0 = all
|
|
if nParams >= 3 && !t.Local(3).IsNil() {
|
|
nNext = t.Local(3).AsNumInt()
|
|
}
|
|
// param 4: nRec — specific record number
|
|
if nParams >= 4 && !t.Local(4).IsNil() {
|
|
nRec := uint32(t.Local(4).AsNumInt())
|
|
area.GoTo(nRec)
|
|
if bFor.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bFor.AsBlock().Fn(t)
|
|
result := t.Pop2()
|
|
area.SetFound(result.AsBool())
|
|
} else {
|
|
area.SetFound(true)
|
|
}
|
|
t.RetBool(area.Found())
|
|
return
|
|
}
|
|
// param 5: lRest — .T. = continue from current, .F. = from top
|
|
lRest := false
|
|
if nParams >= 5 && !t.Local(5).IsNil() {
|
|
lRest = t.Local(5).AsBool()
|
|
}
|
|
|
|
if !lRest && nNext == 0 {
|
|
area.GoTop()
|
|
}
|
|
|
|
// Store locate block for __dbContinue
|
|
if bFor.IsBlock() {
|
|
area.SetLocate("", func(lt *hbrt.Thread) bool {
|
|
lt.PendingParams2(0)
|
|
bFor.AsBlock().Fn(lt)
|
|
return lt.Pop2().AsBool()
|
|
})
|
|
}
|
|
|
|
found := false
|
|
count := int64(0)
|
|
for !area.EOF() {
|
|
if nNext > 0 && count >= nNext {
|
|
break
|
|
}
|
|
// Check WHILE condition
|
|
if bWhile.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bWhile.AsBlock().Fn(t)
|
|
if !t.Pop2().AsBool() {
|
|
break
|
|
}
|
|
}
|
|
// Check FOR condition
|
|
if bFor.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bFor.AsBlock().Fn(t)
|
|
if t.Pop2().AsBool() {
|
|
found = true
|
|
break
|
|
}
|
|
} else {
|
|
found = true
|
|
break
|
|
}
|
|
area.Skip(1)
|
|
count++
|
|
}
|
|
area.SetFound(found)
|
|
t.RetBool(found)
|
|
}
|
|
|
|
// __DBCONTINUE — continue previous LOCATE search.
|
|
// Harbour: __dbContinue()
|
|
func rtlDbContinue(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
|
|
blk := area.LocateBlock()
|
|
if blk == nil {
|
|
area.SetFound(false)
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
|
|
// Skip past current record
|
|
area.Skip(1)
|
|
|
|
found := false
|
|
for !area.EOF() {
|
|
if blk(t) {
|
|
found = true
|
|
break
|
|
}
|
|
area.Skip(1)
|
|
}
|
|
area.SetFound(found)
|
|
t.RetBool(found)
|
|
}
|
|
|
|
// rtlDbAverage implements __dbAverage(bExpr, bFor, bWhile, nNext, nRec,
|
|
// lRest) — sum the expression over visible records and return the
|
|
// arithmetic mean. Returns 0 when the loop visits no records (mirrors
|
|
// Harbour's idiom of avoiding a divide-by-zero in the expansion).
|
|
//
|
|
// Used by `AVERAGE <x> TO <v>` in std.ch.
|
|
func rtlDbAverage(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetDouble(0, 10, 2)
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetDouble(0, 10, 2)
|
|
return
|
|
}
|
|
|
|
bExpr := t.Local(1)
|
|
if !bExpr.IsBlock() {
|
|
t.RetDouble(0, 10, 2)
|
|
return
|
|
}
|
|
var bFor, bWhile hbrt.Value
|
|
if nParams >= 2 {
|
|
bFor = t.Local(2)
|
|
}
|
|
if nParams >= 3 {
|
|
bWhile = t.Local(3)
|
|
}
|
|
nCount := -1
|
|
if nParams >= 4 && !t.Local(4).IsNil() {
|
|
nCount = t.Local(4).AsInt()
|
|
}
|
|
if nParams >= 5 && !t.Local(5).IsNil() {
|
|
area.GoTo(uint32(t.Local(5).AsInt()))
|
|
}
|
|
lRest := false
|
|
if nParams >= 6 && !t.Local(6).IsNil() {
|
|
lRest = t.Local(6).AsBool()
|
|
}
|
|
if !lRest && (nParams < 5 || t.Local(5).IsNil()) {
|
|
area.GoTop()
|
|
}
|
|
|
|
sum := 0.0
|
|
n := 0
|
|
scanned := 0
|
|
for !area.EOF() {
|
|
if nCount >= 0 && scanned >= nCount {
|
|
break
|
|
}
|
|
// WHILE
|
|
if bWhile.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bWhile.AsBlock().Fn(t)
|
|
if !t.GetRetValue().AsBool() {
|
|
break
|
|
}
|
|
}
|
|
// FOR
|
|
eval := true
|
|
if bFor.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bFor.AsBlock().Fn(t)
|
|
eval = t.GetRetValue().AsBool()
|
|
}
|
|
if eval {
|
|
t.PendingParams2(0)
|
|
bExpr.AsBlock().Fn(t)
|
|
sum += t.GetRetValue().AsNumDouble()
|
|
n++
|
|
}
|
|
area.Skip(1)
|
|
scanned++
|
|
}
|
|
if n == 0 {
|
|
t.RetDouble(0, 10, 2)
|
|
return
|
|
}
|
|
t.RetDouble(sum/float64(n), 10, 2)
|
|
}
|
|
|
|
// rtlDbCopy implements __dbCopy(cFile, aFields, bFor, bWhile, nNext,
|
|
// xRec, lRest) — copy visible records from the current workarea into a
|
|
// freshly created DBF. Field projection: an empty/missing aFields
|
|
// copies the whole structure; otherwise only fields whose names match
|
|
// (case-insensitive) are carried over. Used by `COPY TO <f> [FIELDS]
|
|
// [FOR] [WHILE] [NEXT] [RECORD] [REST] [ALL]` in std.ch.
|
|
//
|
|
// Harbour's __dbCopy also accepts cRDD / nConnection / cCodepage / xDelim
|
|
// (params 8..11). Five only supports DBFNTX→DBFNTX for now; SDF/DELIMITED
|
|
// copies stay parser no-ops until that backend lands.
|
|
func rtlDbCopy(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
srcArea := wam.Current()
|
|
if srcArea == nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
|
|
if nParams < 1 || t.Local(1).IsNil() {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
cFile := t.Local(1).AsString()
|
|
if cFile == "" {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
|
|
// Field projection. Harbour passes `{ <(fields)> }` so each entry
|
|
// is a string literal already; uppercase for case-insensitive
|
|
// matching against the source's field names.
|
|
var srcIdx []int
|
|
var dstFields []hbrdd.FieldInfo
|
|
nSrcFields := srcArea.FieldCount()
|
|
useAll := true
|
|
if nParams >= 2 && t.Local(2).IsArray() {
|
|
arr := t.Local(2).AsArray()
|
|
if arr != nil && len(arr.Items) > 0 {
|
|
useAll = false
|
|
wanted := make(map[string]struct{}, len(arr.Items))
|
|
for _, it := range arr.Items {
|
|
s := strings.ToUpper(strings.TrimSpace(it.AsString()))
|
|
if s != "" {
|
|
wanted[s] = struct{}{}
|
|
}
|
|
}
|
|
for i := 0; i < nSrcFields; i++ {
|
|
fi := srcArea.GetFieldInfo(i)
|
|
if _, ok := wanted[strings.ToUpper(fi.Name)]; ok {
|
|
srcIdx = append(srcIdx, i)
|
|
dstFields = append(dstFields, fi)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if useAll {
|
|
srcIdx = make([]int, nSrcFields)
|
|
dstFields = make([]hbrdd.FieldInfo, nSrcFields)
|
|
for i := 0; i < nSrcFields; i++ {
|
|
srcIdx[i] = i
|
|
dstFields[i] = srcArea.GetFieldInfo(i)
|
|
}
|
|
}
|
|
if len(dstFields) == 0 {
|
|
// Nothing to copy — empty FIELDS list with no matches.
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
|
|
// Loop bounds — same shape as dbEval.
|
|
var bFor, bWhile hbrt.Value
|
|
if nParams >= 3 {
|
|
bFor = t.Local(3)
|
|
}
|
|
if nParams >= 4 {
|
|
bWhile = t.Local(4)
|
|
}
|
|
nCount := -1
|
|
if nParams >= 5 && !t.Local(5).IsNil() {
|
|
nCount = t.Local(5).AsInt()
|
|
}
|
|
if nParams >= 6 && !t.Local(6).IsNil() {
|
|
srcArea.GoTo(uint32(t.Local(6).AsInt()))
|
|
}
|
|
lRest := false
|
|
if nParams >= 7 && !t.Local(7).IsNil() {
|
|
lRest = t.Local(7).AsBool()
|
|
}
|
|
if !lRest && (nParams < 6 || t.Local(6).IsNil()) {
|
|
srcArea.GoTop()
|
|
}
|
|
|
|
// Create + open the destination. Use a temp alias so we don't
|
|
// clash with whatever the caller may have open under a name
|
|
// matching the file's basename.
|
|
drv, err := hbrdd.GetDriver("DBFNTX")
|
|
if err != nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
if _, err := drv.Create(hbrdd.CreateParams{Path: cFile, Fields: dstFields}); err != nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
srcSel := wam.CurrentNum()
|
|
dstSel, err := wam.Open("DBFNTX", cFile, "__copytmp", false, false)
|
|
if err != nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
dstArea := wam.AreaAt(dstSel)
|
|
wam.SelectByNum(srcSel)
|
|
|
|
scanned := 0
|
|
for !srcArea.EOF() {
|
|
if nCount >= 0 && scanned >= nCount {
|
|
break
|
|
}
|
|
if bWhile.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bWhile.AsBlock().Fn(t)
|
|
if !t.GetRetValue().AsBool() {
|
|
break
|
|
}
|
|
}
|
|
emit := true
|
|
if bFor.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bFor.AsBlock().Fn(t)
|
|
emit = t.GetRetValue().AsBool()
|
|
}
|
|
if emit {
|
|
vals := make([]hbrt.Value, len(srcIdx))
|
|
for i, idx := range srcIdx {
|
|
v, _ := srcArea.GetValue(idx)
|
|
vals[i] = v
|
|
}
|
|
wam.SelectByNum(dstSel)
|
|
dstArea.Append()
|
|
for i, v := range vals {
|
|
dstArea.PutValue(i, v)
|
|
}
|
|
wam.SelectByNum(srcSel)
|
|
}
|
|
srcArea.Skip(1)
|
|
scanned++
|
|
}
|
|
|
|
// Close the destination, leaving the source selected as on entry.
|
|
wam.SelectByNum(dstSel)
|
|
wam.Close()
|
|
wam.SelectByNum(srcSel)
|
|
t.RetBool(true)
|
|
}
|
|
|
|
// rtlDbSort implements __dbSort(cFile, aFields, bFor, bWhile, nNext,
|
|
// xRec, lRest) — same loop semantics as __dbCopy but the visible
|
|
// records are buffered, sorted by the named keys, and written in
|
|
// order. Each entry of aFields may be a plain field name (ascending)
|
|
// or `name/D` for descending. Unrecognized suffixes are ignored.
|
|
//
|
|
// Used by `SORT TO <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] entries → (fieldIdx, descending).
|
|
type sortKey struct {
|
|
idx int
|
|
desc bool
|
|
}
|
|
var keys []sortKey
|
|
if nParams >= 2 && t.Local(2).IsArray() {
|
|
for _, item := range t.Local(2).AsArray().Items {
|
|
s := strings.TrimSpace(item.AsString())
|
|
if s == "" {
|
|
continue
|
|
}
|
|
desc := false
|
|
// Suffix `/D` (descending), `/A` (ascending), `/C`
|
|
// (case-insensitive — treated as ascending).
|
|
if i := strings.LastIndexByte(s, '/'); i > 0 {
|
|
suffix := strings.ToUpper(strings.TrimSpace(s[i+1:]))
|
|
switch suffix {
|
|
case "D":
|
|
desc = true
|
|
}
|
|
s = strings.TrimSpace(s[:i])
|
|
}
|
|
upper := strings.ToUpper(s)
|
|
idx := -1
|
|
for k := 0; k < nSrcFields; k++ {
|
|
if strings.EqualFold(dstFields[k].Name, upper) {
|
|
idx = k
|
|
break
|
|
}
|
|
}
|
|
if idx >= 0 {
|
|
keys = append(keys, sortKey{idx: idx, desc: desc})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Loop bounds.
|
|
var bFor, bWhile hbrt.Value
|
|
if nParams >= 3 {
|
|
bFor = t.Local(3)
|
|
}
|
|
if nParams >= 4 {
|
|
bWhile = t.Local(4)
|
|
}
|
|
nCount := -1
|
|
if nParams >= 5 && !t.Local(5).IsNil() {
|
|
nCount = t.Local(5).AsInt()
|
|
}
|
|
if nParams >= 6 && !t.Local(6).IsNil() {
|
|
srcArea.GoTo(uint32(t.Local(6).AsInt()))
|
|
}
|
|
lRest := false
|
|
if nParams >= 7 && !t.Local(7).IsNil() {
|
|
lRest = t.Local(7).AsBool()
|
|
}
|
|
if !lRest && (nParams < 6 || t.Local(6).IsNil()) {
|
|
srcArea.GoTop()
|
|
}
|
|
|
|
// Buffer visible records' field values.
|
|
var rows [][]hbrt.Value
|
|
scanned := 0
|
|
for !srcArea.EOF() {
|
|
if nCount >= 0 && scanned >= nCount {
|
|
break
|
|
}
|
|
if bWhile.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bWhile.AsBlock().Fn(t)
|
|
if !t.GetRetValue().AsBool() {
|
|
break
|
|
}
|
|
}
|
|
emit := true
|
|
if bFor.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bFor.AsBlock().Fn(t)
|
|
emit = t.GetRetValue().AsBool()
|
|
}
|
|
if emit {
|
|
row := make([]hbrt.Value, nSrcFields)
|
|
for i := 0; i < nSrcFields; i++ {
|
|
v, _ := srcArea.GetValue(i)
|
|
row[i] = v
|
|
}
|
|
rows = append(rows, row)
|
|
}
|
|
srcArea.Skip(1)
|
|
scanned++
|
|
}
|
|
|
|
// Sort if any keys were given. Stable so equal keys keep input
|
|
// order. Comparison is type-aware: numeric by AsNumDouble, date by
|
|
// AsNumInt julian, logical by truth, otherwise string.
|
|
if len(keys) > 0 && len(rows) > 1 {
|
|
less := func(i, j int) bool {
|
|
for _, k := range keys {
|
|
a := rows[i][k.idx]
|
|
b := rows[j][k.idx]
|
|
cmp := compareValues(a, b)
|
|
if cmp == 0 {
|
|
continue
|
|
}
|
|
if k.desc {
|
|
cmp = -cmp
|
|
}
|
|
return cmp < 0
|
|
}
|
|
return false
|
|
}
|
|
stableSort(rows, less)
|
|
}
|
|
|
|
// Create + open destination, then append in order.
|
|
drv, err := hbrdd.GetDriver("DBFNTX")
|
|
if err != nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
if _, err := drv.Create(hbrdd.CreateParams{Path: cFile, Fields: dstFields}); err != nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
srcSel := wam.CurrentNum()
|
|
dstSel, err := wam.Open("DBFNTX", cFile, "__sorttmp", false, false)
|
|
if err != nil {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
dstArea := wam.AreaAt(dstSel)
|
|
for _, row := range rows {
|
|
dstArea.Append()
|
|
for i, v := range row {
|
|
dstArea.PutValue(i, v)
|
|
}
|
|
}
|
|
wam.Close()
|
|
wam.SelectByNum(srcSel)
|
|
t.RetBool(true)
|
|
}
|
|
|
|
// rtlDbList implements __dbList(lOff, aBlocks, lAll, bFor, bWhile,
|
|
// nNext, nRec, lRest, lPrn, cFile) — output visible records to
|
|
// stdout. aBlocks is an array of column-evaluation code blocks (one
|
|
// per LIST / DISPLAY column expression). If aBlocks is empty or
|
|
// contains only NIL placeholders, every field of the current
|
|
// workarea is emitted.
|
|
//
|
|
// Used by both `LIST [<v,...>]` and `DISPLAY [<v,...>]` in std.ch.
|
|
// lAll distinguishes them: LIST always passes .T. (all matching
|
|
// records); DISPLAY passes .T. only for `DISPLAY ALL`, otherwise .F.
|
|
// (just the current record).
|
|
//
|
|
// TO PRINTER / TO FILE redirection (lPrn / cFile) is accepted but
|
|
// not yet implemented — both paths still write to stdout. OFF (lOff)
|
|
// suppresses the record-number prefix.
|
|
func rtlDbList(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
srcArea := wam.Current()
|
|
if srcArea == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
|
|
lOff := false
|
|
if nParams >= 1 && !t.Local(1).IsNil() {
|
|
lOff = t.Local(1).AsBool()
|
|
}
|
|
|
|
// Decode column blocks. Empty / `{ NIL }` → fall back to "all fields".
|
|
var blocks []hbrt.Value
|
|
useAllFields := true
|
|
if nParams >= 2 && t.Local(2).IsArray() {
|
|
arr := t.Local(2).AsArray()
|
|
if arr != nil {
|
|
for _, it := range arr.Items {
|
|
if it.IsBlock() {
|
|
blocks = append(blocks, it)
|
|
useAllFields = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
lAll := true
|
|
if nParams >= 3 && !t.Local(3).IsNil() {
|
|
lAll = t.Local(3).AsBool()
|
|
}
|
|
|
|
// Loop bounds — same shape as dbEval.
|
|
var bFor, bWhile hbrt.Value
|
|
if nParams >= 4 {
|
|
bFor = t.Local(4)
|
|
}
|
|
if nParams >= 5 {
|
|
bWhile = t.Local(5)
|
|
}
|
|
nCount := -1
|
|
if nParams >= 6 && !t.Local(6).IsNil() {
|
|
nCount = t.Local(6).AsInt()
|
|
}
|
|
if nParams >= 7 && !t.Local(7).IsNil() {
|
|
srcArea.GoTo(uint32(t.Local(7).AsInt()))
|
|
}
|
|
lRest := false
|
|
if nParams >= 8 && !t.Local(8).IsNil() {
|
|
lRest = t.Local(8).AsBool()
|
|
}
|
|
// DISPLAY without ALL emits exactly one record; LIST always emits
|
|
// the full filtered range. Encode the difference by clamping
|
|
// nCount to 1 when lAll is false and no explicit NEXT was given.
|
|
if !lAll && nCount < 0 {
|
|
nCount = 1
|
|
}
|
|
if !lRest && lAll && (nParams < 7 || t.Local(7).IsNil()) {
|
|
srcArea.GoTop()
|
|
}
|
|
|
|
nFields := srcArea.FieldCount()
|
|
scanned := 0
|
|
for !srcArea.EOF() {
|
|
if nCount >= 0 && scanned >= nCount {
|
|
break
|
|
}
|
|
if bWhile.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bWhile.AsBlock().Fn(t)
|
|
if !t.GetRetValue().AsBool() {
|
|
break
|
|
}
|
|
}
|
|
emit := true
|
|
if bFor.IsBlock() {
|
|
t.PendingParams2(0)
|
|
bFor.AsBlock().Fn(t)
|
|
emit = t.GetRetValue().AsBool()
|
|
}
|
|
if emit {
|
|
parts := []string{}
|
|
if !lOff {
|
|
parts = append(parts, fmt.Sprintf("%6d", srcArea.RecNo()))
|
|
}
|
|
if useAllFields {
|
|
for i := 0; i < nFields; i++ {
|
|
v, _ := srcArea.GetValue(i)
|
|
parts = append(parts, valueToDisplay(v))
|
|
}
|
|
} else {
|
|
for _, blk := range blocks {
|
|
t.PendingParams2(0)
|
|
blk.AsBlock().Fn(t)
|
|
parts = append(parts, valueToDisplay(t.GetRetValue()))
|
|
}
|
|
}
|
|
fmt.Print("\r\n" + strings.Join(parts, " "))
|
|
}
|
|
srcArea.Skip(1)
|
|
scanned++
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// stableSort is a tiny insertion sort for small N (typical DBF SORT
|
|
// targets are interactive datasets). Avoids a sort import dependency.
|
|
func stableSort(rows [][]hbrt.Value, less func(i, j int) bool) {
|
|
for i := 1; i < len(rows); i++ {
|
|
for j := i; j > 0 && less(j, j-1); j-- {
|
|
rows[j], rows[j-1] = rows[j-1], rows[j]
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- DBSETFILTER / DBCLEARFILTER / DBFILTER ---
|
|
|
|
// DBSETFILTER(bCondition [, cCondition])
|
|
func rtlDbSetFilter(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
area := wam.Current()
|
|
if area == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
|
|
if nParams >= 1 && t.Local(1).IsBlock() {
|
|
bFilter := t.Local(1)
|
|
expr := ""
|
|
if nParams >= 2 && t.Local(2).IsString() {
|
|
expr = t.Local(2).AsString()
|
|
}
|
|
area.SetFilter(expr, func(lt *hbrt.Thread) bool {
|
|
lt.PendingParams2(0)
|
|
bFilter.AsBlock().Fn(lt)
|
|
return lt.Pop2().AsBool()
|
|
})
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBCLEARFILTER()
|
|
func rtlDbClearFilter(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
|
|
wam := getWA(t)
|
|
if wam == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
if area := wam.Current(); area != nil {
|
|
area.ClearFilter()
|
|
}
|
|
t.RetNil()
|
|
}
|
|
|
|
// DBFILTER() → cFilterExpression
|
|
func rtlDbFilter(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProcFast()
|
|
|
|
// TODO: return stored filter expression from area
|
|
t.RetString("")
|
|
}
|