From 80a18daf8df2f9da07483442cf26da71b697d733 Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Thu, 30 Apr 2026 17:49:33 +0900 Subject: [PATCH] feat(pp): UPDATE FROM via std.ch + nested-bracket fix in matchSegment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `UPDATE [FROM ] [ON ] [RANDOM] REPLACE WITH [, WITH ]` 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 `[, WITH ]` optional-repeat that std.ch already establishes for SUM and DEFAULT. Five-specific tweak: ON wraps as `{|| _FIELD-> }` 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 WITH [, WITH ]]` 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) --- compiler/parser/parser.go | 3 +- compiler/pp/command.go | 47 +++++++++++++ compiler/pp/std.ch | 21 +++++- hbrtl/database.go | 138 ++++++++++++++++++++++++++++++++++++++ hbrtl/register.go | 1 + 5 files changed, 207 insertions(+), 3 deletions(-) diff --git a/compiler/parser/parser.go b/compiler/parser/parser.go index 929090b..4ffdb86 100644 --- a/compiler/parser/parser.go +++ b/compiler/parser/parser.go @@ -1155,8 +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 "UPDATE", - "LABEL", "REPORT", "ACCEPT", "INPUT", + case "LABEL", "REPORT", "ACCEPT", "INPUT", "RELEASE", "SAVE", "RESTORE", "DIR", "STORE", "NOTE", "TEXT", "ENDTEXT", "WITH", "CLEAR": diff --git a/compiler/pp/command.go b/compiler/pp/command.go index 882c88f..33da1d7 100644 --- a/compiler/pp/command.go +++ b/compiler/pp/command.go @@ -420,6 +420,53 @@ func matchSegment(segment, lineWords []string, startLi int, caseSens bool, outer for pi := 0; pi < len(segment); pi++ { pw := segment[pi] + // Nested optional clause: find the matching `]`, run the + // repeat-loop on the inner body until no progress. Mirrors + // the main matchPattern's `[` branch. Doesn't require any + // remaining input — an absent optional just doesn't iterate. + if pw == "[" { + depth := 1 + bodyStart := pi + 1 + bodyEnd := bodyStart + for bodyEnd < len(segment) && depth > 0 { + if segment[bodyEnd] == "[" { + depth++ + } else if segment[bodyEnd] == "]" { + depth-- + if depth == 0 { + break + } + } + bodyEnd++ + } + innerBody := segment[bodyStart:bodyEnd] + innerOuterTail := segment[bodyEnd+1:] + for li < len(lineWords) { + snapshotLi := li + iterCaps, newLi, ok := matchSegment(innerBody, lineWords, li, caseSens, innerOuterTail) + if !ok { + li = snapshotLi + break + } + if newLi == snapshotLi { + break + } + for k, v := range iterCaps { + if prev, hit := caps[k]; hit && prev != "" { + caps[k] = prev + "\x01" + v + } else { + caps[k] = v + } + } + li = newLi + } + pi = bodyEnd + continue + } + if pw == "]" { + // Stray closer — skip. + continue + } if li >= len(lineWords) { return nil, startLi, false } diff --git a/compiler/pp/std.ch b/compiler/pp/std.ch index 8fa5515..bb54a21 100644 --- a/compiler/pp/std.ch +++ b/compiler/pp/std.ch @@ -103,7 +103,11 @@ 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. */ + produce one row per distinct value. + + Note for callers: Five doesn't auto-resolve a bare identifier + inside a code block to the current workarea's field. Write the + key alias-qualified — `ON src->dept` rather than `ON dept`. */ #command TOTAL [TO <(f)>] [ON ] [FIELDS ] ; [FOR ] [WHILE ] [NEXT ] ; [RECORD ] [] [ALL] => ; @@ -117,6 +121,21 @@ [FOR ] => ; __dbJoin( <(alias)>, <(f)>, { <(fields)> }, <{for}> ) +/* UPDATE FROM walks the named detail alias and applies the + REPLACE ... WITH ... clauses to the matching master record. + Both areas should be sorted on the key for the default forward- + walk; pass RANDOM to scan master from top for each detail key. + + Note: ON is wrapped as `_FIELD->` rather than the bare + `<{key}>` Harbour uses, because the same block must evaluate + against both master and detail. Bare identifiers don't auto-bind + to fields under Five — `_FIELD->` makes the dispatch explicit. */ +#command UPDATE [FROM <(alias)>] [ON ] [] ; + [REPLACE WITH ; + [, WITH ]] => ; + __dbUpdate( <(alias)>, {|| _FIELD-> }, <.rand.>, ; + {|| _FIELD-> := [, _FIELD-> := ] } ) + /* --- bulk maintenance --- */ #command REINDEX => DbReindex() #command PACK => DbPack() diff --git a/hbrtl/database.go b/hbrtl/database.go index 6bef9c8..4f2b689 100644 --- a/hbrtl/database.go +++ b/hbrtl/database.go @@ -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 +// ` WITH ` clause as `_FIELD-> := ` so the assignment +// targets the master row while `` 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 [ON ] [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) { diff --git a/hbrtl/register.go b/hbrtl/register.go index ef85d14..f5c7973 100644 --- a/hbrtl/register.go +++ b/hbrtl/register.go @@ -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),