feat(pp): TOTAL TO via std.ch + __dbTotal RTL

`TOTAL TO <file> ON <key> [FIELDS <list>] [FOR ...] [WHILE ...]
[NEXT ...] [RECORD ...] [REST] [ALL]` joins the family of std.ch
DML rewrites. New RTL primitive __dbTotal:

  * Walk the source under dbEval-style FOR/WHILE/NEXT/RECORD/REST
    bounds. The source must already be sorted/indexed on the key —
    same precondition as Harbour's dbtotal.prg.
  * Track the current group key. On each key change, flush the
    accumulated row to the destination (writing the running totals
    back into the most recently appended record's sum-fields,
    preserving each field's declared length/decimals).
  * On the *first* record of every group, append a fresh dst row
    and copy all non-memo source fields into it; subsequent records
    in the group only contribute to the sums. Net effect: non-summed
    fields take the first record's value, summed fields hold the
    group total. Same shape as harbour-core/src/rdd/dbtotal.prg.
  * Memo fields are dropped from the destination structure (Harbour
    does the same).

Parser cleanup: TOTAL removed from the IDENT-statement no-op switch.

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>
This commit is contained in:
2026-04-30 15:24:41 +09:00
parent 1cc2d94927
commit 699ea90156
4 changed files with 217 additions and 1 deletions

View File

@@ -1155,7 +1155,7 @@ func (p *Parser) parseIdentStmt() ast.Stmt {
// rewritten by compiler/pp/std.ch into function calls before the // rewritten by compiler/pp/std.ch into function calls before the
// parser sees them. // parser sees them.
switch upper { switch upper {
case "TOTAL", "UPDATE", case "UPDATE",
"LABEL", "REPORT", "ACCEPT", "INPUT", "LABEL", "REPORT", "ACCEPT", "INPUT",
"JOIN", "RELEASE", "SAVE", "RESTORE", "JOIN", "RELEASE", "SAVE", "RESTORE",
"DIR", "STORE", "NOTE", "TEXT", "ENDTEXT", "DIR", "STORE", "NOTE", "TEXT", "ENDTEXT",

View File

@@ -99,6 +99,17 @@
__dbList( <.off.>, { <{v}> }, <.all.>, ; __dbList( <.off.>, { <{v}> }, <.all.>, ;
<{for}>, <{while}>, <next>, <rec>, <.rest.> ) <{for}>, <{while}>, <next>, <rec>, <.rest.> )
/* TOTAL TO writes one record per consecutive run of equal key values
from the source. Numeric fields named in FIELDS are summed; every
other (non-memo) field takes the first record's value. The source
must already be sorted/indexed on the key for the grouping to
produce one row per distinct value. */
#command TOTAL [TO <(f)>] [ON <key>] [FIELDS <fields,...>] ;
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
[RECORD <rec>] [<rest:REST>] [ALL] => ;
__dbTotal( <(f)>, <{key}>, { <(fields)> }, ;
<{for}>, <{while}>, <next>, <rec>, <.rest.> )
/* --- bulk maintenance --- */ /* --- bulk maintenance --- */
#command REINDEX => DbReindex() #command REINDEX => DbReindex()
#command PACK => DbPack() #command PACK => DbPack()

View File

@@ -1247,6 +1247,210 @@ func rtlDbList(t *hbrt.Thread) {
t.RetNil() t.RetNil()
} }
// rtlDbTotal implements __dbTotal(cFile, bKey, aFields, bFor, bWhile,
// nNext, xRec, lRest) — emit one record per *consecutive* run of
// equal key values from the current workarea, summing the named
// numeric fields and copying every other field from the run's first
// record. The source must already be sorted/indexed on the key for
// the grouping to produce one row per distinct value (Harbour's
// dbtotal.prg has the same precondition).
//
// Used by `TOTAL TO <f> ON <key> [FIELDS <list>] [FOR/WHILE/...]` in
// std.ch.
func rtlDbTotal(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProcFast()
wam := getWA(t)
if wam == nil {
t.RetBool(false)
return
}
srcArea := wam.Current()
if srcArea == nil {
t.RetBool(false)
return
}
if nParams < 1 || t.Local(1).IsNil() {
t.RetBool(false)
return
}
cFile := t.Local(1).AsString()
if cFile == "" {
t.RetBool(false)
return
}
bKey := t.Local(2)
if !bKey.IsBlock() {
t.RetBool(false)
return
}
// Build dst struct — drop memos like Harbour does.
nFields := srcArea.FieldCount()
var dstFields []hbrdd.FieldInfo
var keptIdx []int
for i := 0; i < nFields; i++ {
fi := srcArea.GetFieldInfo(i)
if fi.Type == 'M' {
continue
}
dstFields = append(dstFields, fi)
keptIdx = append(keptIdx, i)
}
if len(dstFields) == 0 {
t.RetBool(false)
return
}
// Resolve sum-fields → list of (dstIdx, srcIdx) pairs preserving
// declaration order.
type sumPair struct{ dst, src int }
var sums []sumPair
if nParams >= 3 && t.Local(3).IsArray() {
arr := t.Local(3).AsArray()
if arr != nil {
wanted := map[string]struct{}{}
for _, it := range arr.Items {
s := strings.ToUpper(strings.TrimSpace(it.AsString()))
if s != "" {
wanted[s] = struct{}{}
}
}
for di, si := range keptIdx {
if _, ok := wanted[strings.ToUpper(dstFields[di].Name)]; ok {
sums = append(sums, sumPair{dst: di, src: si})
}
}
}
}
// Loop bounds.
var bFor, bWhile hbrt.Value
if nParams >= 4 {
bFor = t.Local(4)
}
if nParams >= 5 {
bWhile = t.Local(5)
}
nCount := -1
if nParams >= 6 && !t.Local(6).IsNil() {
nCount = t.Local(6).AsInt()
}
if nParams >= 7 && !t.Local(7).IsNil() {
srcArea.GoTo(uint32(t.Local(7).AsInt()))
}
lRest := false
if nParams >= 8 && !t.Local(8).IsNil() {
lRest = t.Local(8).AsBool()
}
if !lRest && (nParams < 7 || t.Local(7).IsNil()) {
srcArea.GoTop()
}
// Create + open destination.
drv, err := hbrdd.GetDriver("DBFNTX")
if err != nil {
t.RetBool(false)
return
}
if _, err := drv.Create(hbrdd.CreateParams{Path: cFile, Fields: dstFields}); err != nil {
t.RetBool(false)
return
}
srcSel := wam.CurrentNum()
dstSel, err := wam.Open("DBFNTX", cFile, "__totaltmp", false, false)
if err != nil {
t.RetBool(false)
return
}
dstArea := wam.AreaAt(dstSel)
wam.SelectByNum(srcSel)
// Group walk.
var prevKey hbrt.Value
haveGroup := false
running := make([]float64, len(sums))
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]
dstArea.PutValue(sp.dst, hbrt.MakeDouble(running[i], 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)
wam.Close()
wam.SelectByNum(srcSel)
t.RetBool(true)
}
// stableSort is a tiny insertion sort for small N (typical DBF SORT // stableSort is a tiny insertion sort for small N (typical DBF SORT
// targets are interactive datasets). Avoids a sort import dependency. // targets are interactive datasets). Avoids a sort import dependency.
func stableSort(rows [][]hbrt.Value, less func(i, j int) bool) { func stableSort(rows [][]hbrt.Value, less func(i, j int) bool) {

View File

@@ -202,6 +202,7 @@ func RegisterRTL(vm *hbrt.VM) {
hbrt.Sym("__DBCOPY", hbrt.FsPublic, rtlDbCopy), hbrt.Sym("__DBCOPY", hbrt.FsPublic, rtlDbCopy),
hbrt.Sym("__DBSORT", hbrt.FsPublic, rtlDbSort), hbrt.Sym("__DBSORT", hbrt.FsPublic, rtlDbSort),
hbrt.Sym("__DBLIST", hbrt.FsPublic, rtlDbList), hbrt.Sym("__DBLIST", hbrt.FsPublic, rtlDbList),
hbrt.Sym("__DBTOTAL", hbrt.FsPublic, rtlDbTotal),
hbrt.Sym("DBSETFILTER", hbrt.FsPublic, rtlDbSetFilter), hbrt.Sym("DBSETFILTER", hbrt.FsPublic, rtlDbSetFilter),
hbrt.Sym("DBCLEARFILTER", hbrt.FsPublic, rtlDbClearFilter), hbrt.Sym("DBCLEARFILTER", hbrt.FsPublic, rtlDbClearFilter),
hbrt.Sym("DBFILTER", hbrt.FsPublic, rtlDbFilter), hbrt.Sym("DBFILTER", hbrt.FsPublic, rtlDbFilter),