From ebe12e1108a6323eea5f049c6e25aa839f04591b Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Thu, 30 Apr 2026 16:42:06 +0900 Subject: [PATCH] feat(pp): JOIN WITH ... TO via std.ch + __dbJoin RTL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `JOIN WITH TO [FIELDS ] [FOR ]` becomes a preprocessor rewrite to a new RTL primitive __dbJoin. Cartesian product of the current ("master") workarea and the named "detail" alias, filtered by the FOR expression. Output structure: * No FIELDS clause: master's fields followed by detail's, dropping any detail-side name that clashes with master. * FIELDS list: one column per name in declaration order, resolved against master first then detail. Same shape as harbour-core/src/rdd/dbjoin.prg. Five-specific simplifications: alias->name in FIELDS not yet supported (bare names with master-precedence lookup); RDD/codepage args dropped since Five only has DBFNTX. Note for callers: don't name a workarea `M` or `MEMVAR` — both are Harbour-reserved memvar aliases, so `M->field` and `MEMVAR->field` always go through the memory-variable namespace, not the workarea. This is gengo behavior matching Harbour, not new in this commit. Parser cleanup: JOIN 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 | 2 +- compiler/pp/std.ch | 7 ++ hbrtl/database.go | 199 ++++++++++++++++++++++++++++++++++++++ hbrtl/register.go | 1 + 4 files changed, 208 insertions(+), 1 deletion(-) diff --git a/compiler/parser/parser.go b/compiler/parser/parser.go index 52798c7..929090b 100644 --- a/compiler/parser/parser.go +++ b/compiler/parser/parser.go @@ -1157,7 +1157,7 @@ func (p *Parser) parseIdentStmt() ast.Stmt { switch upper { case "UPDATE", "LABEL", "REPORT", "ACCEPT", "INPUT", - "JOIN", "RELEASE", "SAVE", "RESTORE", + "RELEASE", "SAVE", "RESTORE", "DIR", "STORE", "NOTE", "TEXT", "ENDTEXT", "WITH", "CLEAR": p.advance() diff --git a/compiler/pp/std.ch b/compiler/pp/std.ch index a6256f6..8fa5515 100644 --- a/compiler/pp/std.ch +++ b/compiler/pp/std.ch @@ -110,6 +110,13 @@ __dbTotal( <(f)>, <{key}>, { <(fields)> }, ; <{for}>, <{while}>, , , <.rest.> ) +/* JOIN merges the current ("master") workarea with the named + detail alias into a fresh DBF, emitting one output row per + master/detail pair where FOR evaluates true. */ +#command JOIN [WITH <(alias)>] [TO <(f)>] [FIELDS ] ; + [FOR ] => ; + __dbJoin( <(alias)>, <(f)>, { <(fields)> }, <{for}> ) + /* --- bulk maintenance --- */ #command REINDEX => DbReindex() #command PACK => DbPack() diff --git a/hbrtl/database.go b/hbrtl/database.go index 620be66..6bef9c8 100644 --- a/hbrtl/database.go +++ b/hbrtl/database.go @@ -1451,6 +1451,205 @@ func rtlDbTotal(t *hbrt.Thread) { t.RetBool(true) } +// rtlDbJoin implements __dbJoin(cAlias, cFile, aFields, bFor) — emit +// the cartesian product of the current ("master") workarea and the +// named "detail" workarea, filtered by bFor. Output structure: +// +// * No FIELDS clause: master's fields followed by detail's fields, +// dropping detail-side names that clash with master. +// * FIELDS list: in declaration order, each name is resolved +// against master first then detail. +// +// Same shape as harbour-core/src/rdd/dbjoin.prg. Five-specific +// simplifications: alias->name FIELD notation isn't supported yet +// (bare names with master-precedence lookup); RDD/codepage args +// dropped. +func rtlDbJoin(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() + + // param 1: detail workarea alias + 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 + } + + // param 2: destination file name + if nParams < 2 || t.Local(2).IsNil() { + t.RetBool(false) + return + } + cFile := t.Local(2).AsString() + if cFile == "" { + t.RetBool(false) + return + } + + // Build dst struct + a per-dst-field source descriptor (which + // area to read from at output time). + type srcRef struct { + isMaster bool + idx int + } + var dstFields []hbrdd.FieldInfo + var srcRefs []srcRef + addField := func(fi hbrdd.FieldInfo, isMaster bool, srcIdx int) { + // Skip if name already present (master wins). + for _, e := range dstFields { + if strings.EqualFold(e.Name, fi.Name) { + return + } + } + dstFields = append(dstFields, fi) + srcRefs = append(srcRefs, srcRef{isMaster: isMaster, idx: srcIdx}) + } + + // FIELDS list — empty means union of all fields. + var wanted []string + if nParams >= 3 && t.Local(3).IsArray() { + arr := t.Local(3).AsArray() + if arr != nil { + for _, it := range arr.Items { + s := strings.TrimSpace(it.AsString()) + if s != "" { + wanted = append(wanted, strings.ToUpper(s)) + } + } + } + } + if len(wanted) == 0 { + // All master fields, then detail fields with master-name precedence. + for i := 0; i < master.FieldCount(); i++ { + addField(master.GetFieldInfo(i), true, i) + } + for i := 0; i < detail.FieldCount(); i++ { + addField(detail.GetFieldInfo(i), false, i) + } + } else { + // User-specified order: master first, then detail. + for _, n := range wanted { + found := false + for i := 0; i < master.FieldCount(); i++ { + fi := master.GetFieldInfo(i) + if strings.EqualFold(fi.Name, n) { + addField(fi, true, i) + found = true + break + } + } + if !found { + for i := 0; i < detail.FieldCount(); i++ { + fi := detail.GetFieldInfo(i) + if strings.EqualFold(fi.Name, n) { + addField(fi, false, i) + break + } + } + } + } + } + if len(dstFields) == 0 { + t.RetBool(false) + return + } + + // param 4: FOR block. Empty std.ch rule wraps it as `{|| .T. }`, + // so a missing block here means "always true". Treat NIL-block + // the same way for direct callers. + bFor := hbrt.Value{} + if nParams >= 4 { + bFor = t.Local(4) + } + + // Create + open destination. + 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 + } + dstSel, err := wam.Open("DBFNTX", cFile, "__jointmp", false, false) + if err != nil { + t.RetBool(false) + return + } + dstArea := wam.AreaAt(dstSel) + + wam.SelectByNum(masterSel) + master.GoTop() + for !master.EOF() { + wam.SelectByNum(detailSel) + detail.GoTop() + for !detail.EOF() { + wam.SelectByNum(masterSel) + match := true + if bFor.IsBlock() { + t.PendingParams2(0) + bFor.AsBlock().Fn(t) + match = t.GetRetValue().AsBool() + } + if match { + vals := make([]hbrt.Value, len(srcRefs)) + for k, r := range srcRefs { + if r.isMaster { + v, _ := master.GetValue(r.idx) + vals[k] = v + } else { + v, _ := detail.GetValue(r.idx) + vals[k] = v + } + } + wam.SelectByNum(dstSel) + dstArea.Append() + for k, v := range vals { + dstArea.PutValue(k, v) + } + wam.SelectByNum(masterSel) + } + wam.SelectByNum(detailSel) + detail.Skip(1) + } + wam.SelectByNum(masterSel) + master.Skip(1) + } + + wam.SelectByNum(dstSel) + wam.Close() + 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 22a06ab..ef85d14 100644 --- a/hbrtl/register.go +++ b/hbrtl/register.go @@ -203,6 +203,7 @@ func RegisterRTL(vm *hbrt.VM) { hbrt.Sym("__DBSORT", hbrt.FsPublic, rtlDbSort), hbrt.Sym("__DBLIST", hbrt.FsPublic, rtlDbList), hbrt.Sym("__DBTOTAL", hbrt.FsPublic, rtlDbTotal), + hbrt.Sym("__DBJOIN", hbrt.FsPublic, rtlDbJoin), hbrt.Sym("DBSETFILTER", hbrt.FsPublic, rtlDbSetFilter), hbrt.Sym("DBCLEARFILTER", hbrt.FsPublic, rtlDbClearFilter), hbrt.Sym("DBFILTER", hbrt.FsPublic, rtlDbFilter),