From 699ea90156ec0f8b80c262f4162f6982fa0ce539 Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Thu, 30 Apr 2026 15:24:41 +0900 Subject: [PATCH] feat(pp): TOTAL TO via std.ch + __dbTotal RTL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `TOTAL TO ON [FIELDS ] [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) --- compiler/parser/parser.go | 2 +- compiler/pp/std.ch | 11 ++ hbrtl/database.go | 204 ++++++++++++++++++++++++++++++++++++++ hbrtl/register.go | 1 + 4 files changed, 217 insertions(+), 1 deletion(-) diff --git a/compiler/parser/parser.go b/compiler/parser/parser.go index 717cfd0..52798c7 100644 --- a/compiler/parser/parser.go +++ b/compiler/parser/parser.go @@ -1155,7 +1155,7 @@ func (p *Parser) parseIdentStmt() ast.Stmt { // rewritten by compiler/pp/std.ch into function calls before the // parser sees them. switch upper { - case "TOTAL", "UPDATE", + case "UPDATE", "LABEL", "REPORT", "ACCEPT", "INPUT", "JOIN", "RELEASE", "SAVE", "RESTORE", "DIR", "STORE", "NOTE", "TEXT", "ENDTEXT", diff --git a/compiler/pp/std.ch b/compiler/pp/std.ch index c8fb4b1..a6256f6 100644 --- a/compiler/pp/std.ch +++ b/compiler/pp/std.ch @@ -99,6 +99,17 @@ __dbList( <.off.>, { <{v}> }, <.all.>, ; <{for}>, <{while}>, , , <.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 ] [FIELDS ] ; + [FOR ] [WHILE ] [NEXT ] ; + [RECORD ] [] [ALL] => ; + __dbTotal( <(f)>, <{key}>, { <(fields)> }, ; + <{for}>, <{while}>, , , <.rest.> ) + /* --- bulk maintenance --- */ #command REINDEX => DbReindex() #command PACK => DbPack() diff --git a/hbrtl/database.go b/hbrtl/database.go index 95dcca4..620be66 100644 --- a/hbrtl/database.go +++ b/hbrtl/database.go @@ -1247,6 +1247,210 @@ func rtlDbList(t *hbrt.Thread) { 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 ON [FIELDS ] [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 // targets are interactive datasets). Avoids a sort import dependency. func stableSort(rows [][]hbrt.Value, less func(i, j int) bool) { diff --git a/hbrtl/register.go b/hbrtl/register.go index 212f552..22a06ab 100644 --- a/hbrtl/register.go +++ b/hbrtl/register.go @@ -202,6 +202,7 @@ func RegisterRTL(vm *hbrt.VM) { hbrt.Sym("__DBCOPY", hbrt.FsPublic, rtlDbCopy), hbrt.Sym("__DBSORT", hbrt.FsPublic, rtlDbSort), hbrt.Sym("__DBLIST", hbrt.FsPublic, rtlDbList), + hbrt.Sym("__DBTOTAL", hbrt.FsPublic, rtlDbTotal), hbrt.Sym("DBSETFILTER", hbrt.FsPublic, rtlDbSetFilter), hbrt.Sym("DBCLEARFILTER", hbrt.FsPublic, rtlDbClearFilter), hbrt.Sym("DBFILTER", hbrt.FsPublic, rtlDbFilter),