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

@@ -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":

View File

@@ -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
}

View File

@@ -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 <key>] [FIELDS <fields,...>] ;
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
[RECORD <rec>] [<rest:REST>] [ALL] => ;
@@ -117,6 +121,21 @@
[FOR <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 <key> is wrapped as `_FIELD-><key>` 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 <key>] [<rand:RANDOM>] ;
[REPLACE <f1> WITH <x1> ;
[, <fN> WITH <xN>]] => ;
__dbUpdate( <(alias)>, {|| _FIELD-><key> }, <.rand.>, ;
{|| _FIELD-><f1> := <x1>[, _FIELD-><fN> := <xN>] } )
/* --- bulk maintenance --- */
#command REINDEX => DbReindex()
#command PACK => DbPack()