Files
five/hbrtl/database.go
CharlesKWON c2e7f7ea27 feat(pp): Phase B — COUNT / SUM / AVERAGE via std.ch
Three xBase analytical commands that were silent no-ops in the
parser now execute as Harbour-style PP rewrites:

  COUNT [TO <v>]   [FOR <for>] [WHILE <while>] ... -> dbEval()
  SUM <x> TO <v>   [FOR <for>] [WHILE <while>] ... -> dbEval()
  AVERAGE <x> TO <v> [FOR ...]                     -> __dbAverage()

COUNT and SUM expand to a `<v> := 0 ; dbEval( {|| ... } )` pair
matching harbour-core/include/std.ch verbatim. AVERAGE delegates to
a new RTL function rtlDbAverage (sum + count + divide; returns 0 on
empty match) — the chained-private-variable trick Harbour uses to
keep AVERAGE inline doesn't translate cleanly through Five's PP.

Wiring up these rules surfaced four PP issues that had to be fixed
for the rewrite to even reach the parser:

  * Result template did not implement <{name}> blockify. So a rule
    body like `{|| x := x + <x> }, <{for}>` left the literal text
    `<{for}>` in the output. Added blockify substitution: captured
    -> `{|| <captured> }`, missing -> NIL.
  * findMarkerEnd did not recognise `{`/`}` so unreferenced
    blockify markers were not cleaned up either. Added `{`/`}` to
    its prefix/suffix sets.
  * Optional-clause matching had no view of the outer pattern, so a
    regular marker at the end of `[TO <v>]` would swallow the rest
    of the line — `COUNT TO n FOR x>5` captured `<v>` as
    "n FOR x>5". matchSegment now takes outerTail and stops at its
    first literal.
  * `#command` directives could not span multiple physical lines.
    A trailing `;` is harbour-core's line-continuation marker for
    std.ch and now joins the next line into the directive before
    parsing.

Parser cleanup: COUNT, SUM, AVERAGE removed from the IDENT-statement
no-op switch in parseIdentStmt + parseExprStmt. The remaining xBase
verbs (COPY, SORT, TOTAL, JOIN, LIST, DISPLAY, LABEL, REPORT, ...)
stay in the parser until their RTL backends arrive.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 14:11:20 +09:00

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