feat(pp): COPY TO via std.ch + four PP completeness fixes
`COPY TO <file> [FIELDS <list>] [FOR ...] [WHILE ...] [NEXT ...]
[RECORD ...] [REST] [ALL]` reaches the parser as a plain function
call to a new RTL primitive __dbCopy (rtlDbCopy in hbrtl/database.go).
Implementation: project the field list (case-insensitive name match
against the source's structure, full copy when omitted), dbCreate the
target file with that struct, open it under a temp alias, walk the
source under dbEval-style FOR/WHILE/NEXT/RECORD/REST bounds, and
GetValue/Append/PutValue per record into the target. SDF / DELIMITED
variants stay parser no-ops until those backends arrive.
Wiring up COPY surfaced four longstanding gaps in the PP that had to
be fixed for the rule to even reach the runtime:
* `<(name)>` *pattern* marker was treated as a regular `<name>`
with the parens baked into the captured key, so the matching
result substitution `<(name)>` couldn't find it. parseOneMarker
now strips the parens at parse time so capture key and result
marker share the bare name. The smart-stringify result behavior
is unchanged.
* matchSegment (the optional-clause matcher) bailed on every
non-Regular marker. `[FIELDS <fields,...>]` therefore failed to
match at all and the fields list arrived empty in the result
template. matchSegment now handles MarkerList with paren-balanced
capture and segment+outer literal stop boundaries.
* captureExpression only used the first literal in the pattern
tail as a stop boundary. With std.ch's chain of optional
clauses (`[TO <(f)>] [FIELDS ...] [FOR ...] [WHILE ...] ...`)
the file-name marker was happy to gobble a trailing FOR clause
when FIELDS was absent. It now stops at *any* of the remaining
pattern literals.
* `<(name)>` smart-stringify on a list-typed capture wrapped the
whole comma-joined string in one set of quotes — `{ "a , b" }` —
instead of `{ "a", "b" }`. New helper quoteListElements splits on
top-level commas (paren / bracket / brace / string-balanced) and
quotes each element. applyResult now consults the rule's marker
table to know which captures came from `<name,...>`.
Parser cleanup: COPY removed from the IDENT-statement no-op switch in
both parseIdentStmt and parseExprStmt.
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:
@@ -8,6 +8,8 @@
|
||||
package hbrtl
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"five/hbrt"
|
||||
"five/hbrdd"
|
||||
"five/hbrdd/dbf"
|
||||
@@ -766,6 +768,169 @@ func rtlDbAverage(t *hbrt.Thread) {
|
||||
t.RetDouble(sum/float64(n), 10, 2)
|
||||
}
|
||||
|
||||
// rtlDbCopy implements __dbCopy(cFile, aFields, bFor, bWhile, nNext,
|
||||
// xRec, lRest) — copy visible records from the current workarea into a
|
||||
// freshly created DBF. Field projection: an empty/missing aFields
|
||||
// copies the whole structure; otherwise only fields whose names match
|
||||
// (case-insensitive) are carried over. Used by `COPY TO <f> [FIELDS]
|
||||
// [FOR] [WHILE] [NEXT] [RECORD] [REST] [ALL]` in std.ch.
|
||||
//
|
||||
// Harbour's __dbCopy also accepts cRDD / nConnection / cCodepage / xDelim
|
||||
// (params 8..11). Five only supports DBFNTX→DBFNTX for now; SDF/DELIMITED
|
||||
// copies stay parser no-ops until that backend lands.
|
||||
func rtlDbCopy(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
|
||||
}
|
||||
|
||||
// Field projection. Harbour passes `{ <(fields)> }` so each entry
|
||||
// is a string literal already; uppercase for case-insensitive
|
||||
// matching against the source's field names.
|
||||
var srcIdx []int
|
||||
var dstFields []hbrdd.FieldInfo
|
||||
nSrcFields := srcArea.FieldCount()
|
||||
useAll := true
|
||||
if nParams >= 2 && t.Local(2).IsArray() {
|
||||
arr := t.Local(2).AsArray()
|
||||
if arr != nil && len(arr.Items) > 0 {
|
||||
useAll = false
|
||||
wanted := make(map[string]struct{}, len(arr.Items))
|
||||
for _, it := range arr.Items {
|
||||
s := strings.ToUpper(strings.TrimSpace(it.AsString()))
|
||||
if s != "" {
|
||||
wanted[s] = struct{}{}
|
||||
}
|
||||
}
|
||||
for i := 0; i < nSrcFields; i++ {
|
||||
fi := srcArea.GetFieldInfo(i)
|
||||
if _, ok := wanted[strings.ToUpper(fi.Name)]; ok {
|
||||
srcIdx = append(srcIdx, i)
|
||||
dstFields = append(dstFields, fi)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if useAll {
|
||||
srcIdx = make([]int, nSrcFields)
|
||||
dstFields = make([]hbrdd.FieldInfo, nSrcFields)
|
||||
for i := 0; i < nSrcFields; i++ {
|
||||
srcIdx[i] = i
|
||||
dstFields[i] = srcArea.GetFieldInfo(i)
|
||||
}
|
||||
}
|
||||
if len(dstFields) == 0 {
|
||||
// Nothing to copy — empty FIELDS list with no matches.
|
||||
t.RetBool(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Loop bounds — same shape as dbEval.
|
||||
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()
|
||||
}
|
||||
|
||||
// Create + open the destination. Use a temp alias so we don't
|
||||
// clash with whatever the caller may have open under a name
|
||||
// matching the file's basename.
|
||||
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, "__copytmp", false, false)
|
||||
if err != nil {
|
||||
t.RetBool(false)
|
||||
return
|
||||
}
|
||||
dstArea := wam.AreaAt(dstSel)
|
||||
wam.SelectByNum(srcSel)
|
||||
|
||||
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 {
|
||||
vals := make([]hbrt.Value, len(srcIdx))
|
||||
for i, idx := range srcIdx {
|
||||
v, _ := srcArea.GetValue(idx)
|
||||
vals[i] = v
|
||||
}
|
||||
wam.SelectByNum(dstSel)
|
||||
dstArea.Append()
|
||||
for i, v := range vals {
|
||||
dstArea.PutValue(i, v)
|
||||
}
|
||||
wam.SelectByNum(srcSel)
|
||||
}
|
||||
srcArea.Skip(1)
|
||||
scanned++
|
||||
}
|
||||
|
||||
// Close the destination, leaving the source selected as on entry.
|
||||
wam.SelectByNum(dstSel)
|
||||
wam.Close()
|
||||
wam.SelectByNum(srcSel)
|
||||
t.RetBool(true)
|
||||
}
|
||||
|
||||
// --- DBSETFILTER / DBCLEARFILTER / DBFILTER ---
|
||||
|
||||
// DBSETFILTER(bCondition [, cCondition])
|
||||
|
||||
@@ -199,6 +199,7 @@ func RegisterRTL(vm *hbrt.VM) {
|
||||
hbrt.Sym("__DBLOCATE", hbrt.FsPublic, rtlDbLocate),
|
||||
hbrt.Sym("__DBCONTINUE", hbrt.FsPublic, rtlDbContinue),
|
||||
hbrt.Sym("__DBAVERAGE", hbrt.FsPublic, rtlDbAverage),
|
||||
hbrt.Sym("__DBCOPY", hbrt.FsPublic, rtlDbCopy),
|
||||
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