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