feat(pp): SORT TO via std.ch + __dbSort RTL
`SORT TO <file> [ON <key-list>] [FOR ...] [WHILE ...] [NEXT ...]
[RECORD ...] [REST] [ALL]` joins COPY in being a real preprocessor
rewrite to a function call. New RTL primitive __dbSort:
* Buffer visible source records (FOR/WHILE/NEXT/RECORD/REST same
as __dbCopy).
* Multi-key stable insertion sort. Each key may carry `/D` for
descending; ascending otherwise. /A and unknown suffixes fall
through as ascending. Comparison delegates to the existing
compareValues helper in sqlscan.go (numeric / string / NIL-aware).
* Create destination DBF with the source's struct, append rows in
sorted order, restore source selection.
Parser cleanup: SORT 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
|
||||
// parser sees them.
|
||||
switch upper {
|
||||
case "SORT", "TOTAL", "UPDATE",
|
||||
case "TOTAL", "UPDATE",
|
||||
"LABEL", "REPORT", "ACCEPT", "INPUT",
|
||||
"JOIN", "RELEASE", "SAVE", "RESTORE",
|
||||
"DIR", "STORE", "NOTE", "TEXT", "ENDTEXT",
|
||||
|
||||
@@ -73,6 +73,15 @@
|
||||
__dbCopy( <(f)>, { <(fields)> }, ;
|
||||
<{for}>, <{while}>, <next>, <rec>, <.rest.> )
|
||||
|
||||
/* SORT TO copies the visible records into a fresh DBF in key order.
|
||||
Each key in `<fields>` may carry `/D` for descending; default is
|
||||
ascending. */
|
||||
#command SORT [TO <(f)>] [ON <fields,...>] ;
|
||||
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
|
||||
[RECORD <rec>] [<rest:REST>] [ALL] => ;
|
||||
__dbSort( <(f)>, { <(fields)> }, ;
|
||||
<{for}>, <{while}>, <next>, <rec>, <.rest.> )
|
||||
|
||||
/* --- bulk maintenance --- */
|
||||
#command REINDEX => DbReindex()
|
||||
#command PACK => DbPack()
|
||||
|
||||
@@ -931,6 +931,202 @@ func rtlDbCopy(t *hbrt.Thread) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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])
|
||||
|
||||
@@ -200,6 +200,7 @@ func RegisterRTL(vm *hbrt.VM) {
|
||||
hbrt.Sym("__DBCONTINUE", hbrt.FsPublic, rtlDbContinue),
|
||||
hbrt.Sym("__DBAVERAGE", hbrt.FsPublic, rtlDbAverage),
|
||||
hbrt.Sym("__DBCOPY", hbrt.FsPublic, rtlDbCopy),
|
||||
hbrt.Sym("__DBSORT", hbrt.FsPublic, rtlDbSort),
|
||||
hbrt.Sym("DBSETFILTER", hbrt.FsPublic, rtlDbSetFilter),
|
||||
hbrt.Sym("DBCLEARFILTER", hbrt.FsPublic, rtlDbClearFilter),
|
||||
hbrt.Sym("DBFILTER", hbrt.FsPublic, rtlDbFilter),
|
||||
|
||||
Reference in New Issue
Block a user