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:
2026-04-30 15:00:18 +09:00
parent c2e7f7ea27
commit e961660f61
5 changed files with 357 additions and 23 deletions

View File

@@ -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])

View File

@@ -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),