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:
@@ -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",
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user