fix: 5 seek/dbf bugs — 77/77 thorough Harbour compatibility

1. SOFTSEEK: use idx.CurRecNo() for positioning (was checking recNo > 0)
   - SEEK with SET SOFTSEEK ON now positions at next higher key
   - SEEK command reads SET SOFTSEEK at runtime (was compile-time only)
   - rtlDbSeek defaults to GetSetSoftSeek() when no explicit param

2. SET DELETED ON + INDEX: SkipIndexed skips deleted records
   - GoTopIndexed: skip deleted record at top position
   - SkipIndexed: inner loop continues past deleted records

3. Compound key (CITY+NAME): field name TrimSpace before lookup
   - evalKeyExprInner: TrimSpace on fieldName after FIELD-> strip
   - Fixed "CITY " != "CITY" mismatch from + operator splitting

4. SET INDEX TO filename: treated as string, not variable
   - gengo uses exprToString for SET INDEX TO (was emitExpr)
   - Prevents identifier being resolved as local variable

5. hasXBaseCommands: recursive scan into nested blocks
   - BEGIN SEQUENCE, IF, FOR, DO WHILE, SWITCH bodies now scanned
   - Fixes missing hbrdd import for DB commands inside blocks

Thorough test: 77 items (14 sections) covering exact/partial/soft seek,
SET DELETED, duplicate keys, numeric keys, compound keys, empty/single
table, state consistency, order switching, full traversal — all identical.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 14:08:51 +09:00
parent c04c9aeaa8
commit b7028791d6
6 changed files with 393 additions and 27 deletions

View File

@@ -416,11 +416,15 @@ func (a *DBFArea) Seek(key hbrt.Value, softSeek bool, findLast bool) (bool, erro
return true, nil
}
if softSeek && recNo > 0 && !idx.IsEOF() {
a.GoTo(recNo)
a.FEof = false
a.SetFound(false)
return false, nil
if softSeek && !idx.IsEOF() {
// Softseek: position at the next higher key
posRecNo := idx.CurRecNo()
if posRecNo > 0 {
a.GoTo(posRecNo)
a.FEof = false
a.SetFound(false)
return false, nil
}
}
// Not found — go to EOF
@@ -464,7 +468,12 @@ func (a *DBFArea) GoTopIndexed() error {
a.FEof = true
return a.GoTo(rc + 1)
}
return a.GoTo(idx.CurRecNo())
a.GoTo(idx.CurRecNo())
// Skip deleted records at top
if hbrdd.IsSetDeleted != nil && hbrdd.IsSetDeleted() && a.Deleted() {
return a.SkipIndexed(1)
}
return nil
}
// GoBottomIndexed positions at the last key in the active index.
@@ -525,23 +534,35 @@ func (a *DBFArea) SkipIndexed(count int64) error {
idx := a.idxState.indexes[a.idxState.current]
hasScope := a.idxState.scopeTop != nil || a.idxState.scopeBottom != nil
setDel := hbrdd.IsSetDeleted != nil && hbrdd.IsSetDeleted()
if count > 0 {
for i := int64(0); i < count; i++ {
idx.SkipNext()
if idx.IsEOF() || idx.CurRecNo() == 0 {
rc, _ := a.RecCount()
a.GoTo(rc + 1)
a.FEof = true
return nil
}
// Check bottom scope
if hasScope && a.idxState.scopeBottom != nil {
if bytes.Compare(idx.CurKey(), a.idxState.scopeBottom) > 0 {
for {
idx.SkipNext()
if idx.IsEOF() || idx.CurRecNo() == 0 {
rc, _ := a.RecCount()
a.GoTo(rc + 1)
a.FEof = true
return nil
}
// Check bottom scope
if hasScope && a.idxState.scopeBottom != nil {
if bytes.Compare(idx.CurKey(), a.idxState.scopeBottom) > 0 {
rc, _ := a.RecCount()
a.GoTo(rc + 1)
a.FEof = true
return nil
}
}
// Skip deleted records
if setDel {
a.GoTo(idx.CurRecNo())
if a.Deleted() {
continue
}
}
break
}
}
} else if count < 0 {
@@ -737,9 +758,9 @@ func (a *DBFArea) evalKeyExprInner(expr string) []byte {
}
// Strip FIELD-> or _FIELD-> or alias-> prefix (Harbour: M->var, FIELD->var)
fieldName := upper
fieldName := strings.TrimSpace(upper)
if idx := strings.Index(fieldName, "->"); idx >= 0 {
fieldName = fieldName[idx+2:]
fieldName = strings.TrimSpace(fieldName[idx+2:])
}
// Simple field name