feat(pp): JOIN WITH ... TO via std.ch + __dbJoin RTL

`JOIN WITH <alias> TO <file> [FIELDS <list>] [FOR <expr>]` 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) <noreply@anthropic.com>
This commit is contained in:
2026-04-30 16:42:06 +09:00
parent 699ea90156
commit ebe12e1108
4 changed files with 208 additions and 1 deletions

View File

@@ -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) {

View File

@@ -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),