feat(pp): UPDATE FROM via std.ch + nested-bracket fix in matchSegment

`UPDATE [FROM <alias>] [ON <key>] [RANDOM] REPLACE <f1> WITH <x1>
[, <fN> WITH <xN>]` becomes a preprocessor rewrite to a new RTL
primitive __dbUpdate. For each detail record, find the master
record with matching key (forward-walk if both sorted, full scan
when RANDOM) and apply the REPLACE clauses in master's context.

Same shape as harbour-core/src/rdd/dbupdat.prg. The REPLACE clauses
expand to comma-separated assignments inside one block —
`{|| _FIELD->total := del->amt, _FIELD->status := "OK" }` — using
the multi-pair `[, <fN> WITH <xN>]` optional-repeat that std.ch
already establishes for SUM and DEFAULT.

Five-specific tweak: ON <key> wraps as `{|| _FIELD-><key> }` rather
than Harbour's bare `<{key}>`. Five doesn't auto-resolve a bare
identifier in a code block to the current workarea's field, and the
UPDATE block must evaluate against both detail and master so an
explicit alias prefix won't do — _FIELD-> dispatches to whichever
area is selected at eval time, which is what's needed.

Wiring up UPDATE surfaced one further matchSegment gap that fell
out of the multi-pair `[REPLACE ... [, ...]]` shape:

  * matchSegment didn't handle nested `[...]` inside its body.
    `[REPLACE <f1> WITH <x1> [, <fN> WITH <xN>]]` gave the inner
    `[` as a literal token to match against the line, so even the
    single-pair `REPLACE total WITH del->amt` form failed and f1/x1
    came back empty. Now matchSegment runs the same repeat-loop on
    inner `[...]` blocks that the top-level matcher uses, with its
    own outer-tail computed from the segment tail past the inner
    `]`.

Parser cleanup: UPDATE 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:
2026-04-30 17:49:33 +09:00
parent ebe12e1108
commit 80a18daf8d
5 changed files with 207 additions and 3 deletions

View File

@@ -1650,6 +1650,144 @@ func rtlDbJoin(t *hbrt.Thread) {
t.RetBool(true)
}
// rtlDbUpdate implements __dbUpdate(cAlias, bKey, lRandom, bAssign) —
// for each record in the detail workarea, find the matching master
// record (by key equality) and apply bAssign in master's context.
// Same shape as harbour-core/src/rdd/dbupdat.prg:
//
// * bKey runs in either context — typically a bare field name that
// exists in both areas.
// * bAssign runs in *master* context — Harbour's std.ch wraps each
// `<f> WITH <x>` clause as `_FIELD-><f> := <x>` so the assignment
// targets the master row while `<x>` is free to read detail->...
// * lRandom .T.: scan master from top for each detail key. Otherwise
// both areas must be sorted on the key; the master pointer just
// walks forward through equal-or-greater keys (matches Harbour's
// forward-walk semantics for the non-random branch).
//
// Used by `UPDATE FROM <alias> [ON <key>] [RANDOM] REPLACE ...`
// in std.ch.
func rtlDbUpdate(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProcFast()
wam := getWA(t)
if wam == nil {
t.RetBool(false)
return
}
master := wam.Current()
if master == nil {
t.RetBool(false)
return
}
masterSel := wam.CurrentNum()
if nParams < 1 || t.Local(1).IsNil() {
t.RetBool(false)
return
}
cAlias := strings.TrimSpace(t.Local(1).AsString())
if cAlias == "" {
t.RetBool(false)
return
}
detailSel := wam.FindByAlias(cAlias)
if detailSel == 0 {
t.RetBool(false)
return
}
detail := wam.AreaAt(detailSel)
if detail == nil {
t.RetBool(false)
return
}
bKey := hbrt.Value{}
if nParams >= 2 {
bKey = t.Local(2)
}
if !bKey.IsBlock() {
// No key block — nothing to drive the update by.
t.RetBool(false)
return
}
lRandom := false
if nParams >= 3 && !t.Local(3).IsNil() {
lRandom = t.Local(3).AsBool()
}
bAssign := hbrt.Value{}
if nParams >= 4 {
bAssign = t.Local(4)
}
if !bAssign.IsBlock() {
t.RetBool(false)
return
}
// Position both areas at top.
master.GoTop()
wam.SelectByNum(detailSel)
detail.GoTop()
wam.SelectByNum(masterSel)
for {
wam.SelectByNum(detailSel)
if detail.EOF() {
break
}
// Key from the detail row.
t.PendingParams2(0)
bKey.AsBlock().Fn(t)
detailKey := t.GetRetValue()
wam.SelectByNum(masterSel)
if lRandom {
// Linear scan from top — index-aware seek would be
// faster but Five's seek API isn't part of the Area
// interface yet.
master.GoTop()
for !master.EOF() {
t.PendingParams2(0)
bKey.AsBlock().Fn(t)
if compareValues(t.GetRetValue(), detailKey) == 0 {
break
}
master.Skip(1)
}
} else {
// Walk forward while master's key < detail's key.
for !master.EOF() {
t.PendingParams2(0)
bKey.AsBlock().Fn(t)
if compareValues(t.GetRetValue(), detailKey) >= 0 {
break
}
master.Skip(1)
}
}
if !master.EOF() {
// Match check (also covers the random branch's
// "found something" outcome).
t.PendingParams2(0)
bKey.AsBlock().Fn(t)
if compareValues(t.GetRetValue(), detailKey) == 0 {
t.PendingParams2(0)
bAssign.AsBlock().Fn(t)
}
}
wam.SelectByNum(detailSel)
detail.Skip(1)
}
wam.SelectByNum(masterSel)
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) {

View File

@@ -204,6 +204,7 @@ func RegisterRTL(vm *hbrt.VM) {
hbrt.Sym("__DBLIST", hbrt.FsPublic, rtlDbList),
hbrt.Sym("__DBTOTAL", hbrt.FsPublic, rtlDbTotal),
hbrt.Sym("__DBJOIN", hbrt.FsPublic, rtlDbJoin),
hbrt.Sym("__DBUPDATE", hbrt.FsPublic, rtlDbUpdate),
hbrt.Sym("DBSETFILTER", hbrt.FsPublic, rtlDbSetFilter),
hbrt.Sym("DBCLEARFILTER", hbrt.FsPublic, rtlDbClearFilter),
hbrt.Sym("DBFILTER", hbrt.FsPublic, rtlDbFilter),