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:
@@ -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":
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user