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

@@ -98,7 +98,12 @@ func (g *Generator) emitSeekCmd(s *ast.SeekCmd, locals localMap) {
g.writeln("_key := t.Pop2()") g.writeln("_key := t.Pop2()")
g.writeln("if _idx, ok := area.(hbrdd.Indexer); ok {") g.writeln("if _idx, ok := area.(hbrdd.Indexer); ok {")
g.indent++ g.indent++
g.writeln(fmt.Sprintf("_found, _ := _idx.Seek(_key, %v, false)", s.SoftSeek)) // SoftSeek: from SEEK SOFT keyword OR runtime SET SOFTSEEK
if s.SoftSeek {
g.writeln("_found, _ := _idx.Seek(_key, true, false)")
} else {
g.writeln("_found, _ := _idx.Seek(_key, hbrtl.GetSetSoftSeek(), false)")
}
g.writeln("_ = _found") g.writeln("_ = _found")
g.indent-- g.indent--
g.writeln("}") g.writeln("}")

View File

@@ -12,13 +12,51 @@ func hasXBaseCommands(file *ast.File) bool {
if !ok { if !ok {
continue continue
} }
for _, s := range fn.Body { if scanStmtsForXBase(fn.Body) {
switch s.(type) { return true
case *ast.UseCmd, *ast.GoCmd, *ast.SkipCmd, *ast.SeekCmd, }
*ast.ReplaceCmd, *ast.AppendCmd, *ast.DeleteCmd, }
*ast.SelectCmd, *ast.IndexCmd, *ast.SetCmd: return false
}
func scanStmtsForXBase(stmts []ast.Stmt) bool {
for _, s := range stmts {
switch v := s.(type) {
case *ast.UseCmd, *ast.GoCmd, *ast.SkipCmd, *ast.SeekCmd,
*ast.ReplaceCmd, *ast.AppendCmd, *ast.DeleteCmd,
*ast.SelectCmd, *ast.IndexCmd, *ast.SetCmd:
return true
case *ast.IfStmt:
if scanStmtsForXBase(v.Body) || scanStmtsForXBase(v.ElseBody) {
return true return true
} }
for _, ei := range v.ElseIfs {
if scanStmtsForXBase(ei.Body) {
return true
}
}
case *ast.ForStmt:
if scanStmtsForXBase(v.Body) {
return true
}
case *ast.ForEachStmt:
if scanStmtsForXBase(v.Body) {
return true
}
case *ast.DoWhileStmt:
if scanStmtsForXBase(v.Body) {
return true
}
case *ast.SeqStmt:
if scanStmtsForXBase(v.Body) || scanStmtsForXBase(v.RecoverBody) {
return true
}
case *ast.SwitchStmt:
for _, c := range v.Cases {
if scanStmtsForXBase(c.Body) {
return true
}
}
} }
} }
return false return false

View File

@@ -564,10 +564,15 @@ func (g *Generator) emitStmt(stmt ast.Stmt, locals localMap) {
} }
case "INDEX": case "INDEX":
if s.Expr != nil { if s.Expr != nil {
fileStr := exprToString(s.Expr)
g.writeln("if idx, ok := area.(hbrdd.Indexer); ok {") g.writeln("if idx, ok := area.(hbrdd.Indexer); ok {")
g.indent++ g.indent++
g.emitExpr(s.Expr) if fileStr != "" {
g.writeln(`idx.OrderListAdd(t.Pop2().AsString())`) g.writeln(fmt.Sprintf(`idx.OrderListAdd(%q)`, fileStr))
} else {
g.emitExpr(s.Expr)
g.writeln(`idx.OrderListAdd(t.Pop2().AsString())`)
}
g.indent-- g.indent--
g.writeln("}") g.writeln("}")
} else { } else {

297
examples/seek_thorough.prg Normal file
View File

@@ -0,0 +1,297 @@
// Thorough DBF + SEEK compatibility test — Harbour vs Five
PROCEDURE Main()
LOCAL i, aStruct, aCities, nIdx, nCount
ErrorBlock({|e| Break(e)})
aStruct := { ;
{"ID","N",6,0}, {"NAME","C",20,0}, {"CITY","C",15,0}, ;
{"CODE","C",5,0}, {"SALARY","N",10,2} ;
}
BEGIN SEQUENCE
dbCreate("seek_test", aStruct)
USE "seek_test" NEW
aCities := {"Seoul","Tokyo","Beijing","London","NYC"}
FOR i := 1 TO 30
APPEND BLANK
REPLACE ID WITH i
REPLACE NAME WITH PadR("Name_" + PadL(LTrim(Str(i)), 3, "0"), 20)
nIdx := ((i-1) % 5) + 1
REPLACE CITY WITH PadR(aCities[nIdx], 15)
REPLACE CODE WITH PadR(LTrim(Str(Int((i-1)/10)+1)), 5)
REPLACE SALARY WITH 20000 + i * 500.00
NEXT
R("SETUP", LTrim(Str(RecCount())))
// S1: Basic field access
GO 1
R("S1_REC1_ID", LTrim(Str(FieldGet(1))))
R("S1_REC1_NAME", RTrim(FieldGet(2)))
R("S1_REC1_CITY", RTrim(FieldGet(3)))
GO 15
R("S1_REC15_ID", LTrim(Str(FieldGet(1))))
R("S1_REC15_NAME", RTrim(FieldGet(2)))
GO 30
R("S1_REC30_ID", LTrim(Str(FieldGet(1))))
// S2: NAME index — exact seek
INDEX ON FIELD->NAME TO seek_name
R("S2_IDX", "OK")
GO TOP
R("S2_TOP_NAME", RTrim(FieldGet(2)))
R("S2_TOP_RECNO", LTrim(Str(RecNo())))
GO BOTTOM
R("S2_BOT_NAME", RTrim(FieldGet(2)))
R("S2_BOT_RECNO", LTrim(Str(RecNo())))
SEEK PadR("Name_001", 20)
R("S2_SEEK_001", B(Found()) + " " + LTrim(Str(RecNo())) + " " + B(EOF()))
SEEK PadR("Name_015", 20)
R("S2_SEEK_015", B(Found()) + " " + LTrim(Str(RecNo())) + " " + B(EOF()))
SEEK PadR("Name_030", 20)
R("S2_SEEK_030", B(Found()) + " " + LTrim(Str(RecNo())) + " " + B(EOF()))
SEEK PadR("Name_000", 20)
R("S2_MISS_000", B(Found()) + " " + LTrim(Str(RecNo())) + " " + B(EOF()))
SEEK PadR("Name_031", 20)
R("S2_MISS_031", B(Found()) + " " + LTrim(Str(RecNo())) + " " + B(EOF()))
SEEK PadR("Name_999", 20)
R("S2_MISS_999", B(Found()) + " " + LTrim(Str(RecNo())) + " " + B(EOF()))
SEEK PadR("ZZZZZ", 20)
R("S2_MISS_ZZZ", B(Found()) + " " + B(EOF()))
// S3: Partial key
SEEK "Name_0"
R("S3_PART_0", B(Found()) + " " + LTrim(Str(RecNo())) + " " + RTrim(FieldGet(2)))
SEEK "Name_01"
R("S3_PART_01", B(Found()) + " " + LTrim(Str(RecNo())))
SEEK "Name_1"
R("S3_PART_1", B(Found()) + " " + LTrim(Str(RecNo())))
SEEK "Name_3"
R("S3_PART_3", B(Found()) + " " + LTrim(Str(RecNo())))
SEEK "N"
R("S3_PART_N", B(Found()) + " " + LTrim(Str(RecNo())))
SEEK "Z"
R("S3_PART_Z", B(Found()) + " " + B(EOF()))
// S4: SOFTSEEK
SET SOFTSEEK ON
SEEK PadR("Name_001", 20)
R("S4_SOFT_001", B(Found()) + " " + LTrim(Str(RecNo())))
SEEK PadR("Name_000", 20)
R("S4_SOFT_000", B(Found()) + " " + LTrim(Str(RecNo())) + " " + RTrim(FieldGet(2)))
SEEK PadR("Name_015X", 20)
R("S4_SOFT_15X", B(Found()) + " " + LTrim(Str(RecNo())) + " " + RTrim(FieldGet(2)))
SEEK PadR("Name_031", 20)
R("S4_SOFT_031", B(Found()) + " " + B(EOF()))
SEEK PadR("ZZZZZ", 20)
R("S4_SOFT_ZZZ", B(Found()) + " " + B(EOF()))
SEEK "A"
R("S4_SOFT_A", B(Found()) + " " + LTrim(Str(RecNo())) + " " + RTrim(FieldGet(2)))
SET SOFTSEEK OFF
// S5: Seek + navigation
SEEK PadR("Name_010", 20)
R("S5_SEEK_010", B(Found()) + " " + LTrim(Str(RecNo())))
SKIP
R("S5_SKIP1", LTrim(Str(RecNo())) + " " + RTrim(FieldGet(2)))
SKIP -1
R("S5_SKIPM1", LTrim(Str(RecNo())) + " " + RTrim(FieldGet(2)))
SKIP 5
R("S5_SKIP5", LTrim(Str(RecNo())) + " " + RTrim(FieldGet(2)))
// S6: Duplicate key (CITY)
CLOSE ALL
USE "seek_test" NEW
INDEX ON FIELD->CITY TO seek_city
R("S6_IDX", "OK")
GO TOP
R("S6_TOP", RTrim(FieldGet(3)) + " " + LTrim(Str(RecNo())))
SEEK PadR("Seoul", 15)
R("S6_SEOUL", B(Found()) + " " + LTrim(Str(RecNo())))
nCount := 0
DO WHILE !EOF() .AND. RTrim(FieldGet(3)) == "Seoul"
nCount++
SKIP
ENDDO
R("S6_SEOUL_CNT", LTrim(Str(nCount)))
SEEK PadR("Tokyo", 15)
R("S6_TOKYO", B(Found()) + " " + LTrim(Str(RecNo())))
SEEK PadR("Beijing", 15)
R("S6_BEIJING", B(Found()) + " " + LTrim(Str(RecNo())))
SEEK PadR("London", 15)
R("S6_LONDON", B(Found()) + " " + LTrim(Str(RecNo())))
SEEK PadR("NYC", 15)
R("S6_NYC", B(Found()) + " " + LTrim(Str(RecNo())))
SEEK PadR("Paris", 15)
R("S6_PARIS", B(Found()) + " " + B(EOF()))
// S7: SET DELETED + SEEK
CLOSE ALL
USE "seek_test" NEW
INDEX ON FIELD->NAME TO seek_name
SET ORDER TO 0
FOR i := 1 TO 30
GO i
IF i % 5 == 0
DELETE
ENDIF
NEXT
SET ORDER TO 1
SET DELETED OFF
SEEK PadR("Name_005", 20)
R("S7_DELOFF_005", B(Found()) + " " + LTrim(Str(RecNo())))
SEEK PadR("Name_010", 20)
R("S7_DELOFF_010", B(Found()) + " " + LTrim(Str(RecNo())))
SET DELETED ON
GO TOP
nCount := 0
DO WHILE !EOF()
nCount++
SKIP
ENDDO
R("S7_DELON_CNT", LTrim(Str(nCount)))
SET DELETED OFF
SET ORDER TO 0
FOR i := 1 TO 30
GO i
IF Deleted()
RECALL
ENDIF
NEXT
// S8: Numeric key
CLOSE ALL
USE "seek_test" NEW
INDEX ON Str(FIELD->ID, 6) TO seek_id
R("S8_IDX", "OK")
SEEK Str(1, 6)
R("S8_SEEK_1", B(Found()) + " " + LTrim(Str(RecNo())))
SEEK Str(15, 6)
R("S8_SEEK_15", B(Found()) + " " + LTrim(Str(RecNo())))
SEEK Str(30, 6)
R("S8_SEEK_30", B(Found()) + " " + LTrim(Str(RecNo())))
SEEK Str(31, 6)
R("S8_SEEK_31", B(Found()) + " " + B(EOF()))
GO TOP
R("S8_TOP_ID", LTrim(Str(FieldGet(1))))
GO BOTTOM
R("S8_BOT_ID", LTrim(Str(FieldGet(1))))
// S9: Compound key
CLOSE ALL
USE "seek_test" NEW
INDEX ON FIELD->CITY + FIELD->NAME TO seek_compound
R("S9_IDX", "OK")
GO TOP
R("S9_TOP", RTrim(FieldGet(3)) + " " + RTrim(FieldGet(2)) + " " + LTrim(Str(RecNo())))
GO BOTTOM
R("S9_BOT", RTrim(FieldGet(3)) + " " + RTrim(FieldGet(2)) + " " + LTrim(Str(RecNo())))
SEEK PadR("Seoul", 15) + PadR("Name_001", 20)
R("S9_SEEK_S001", B(Found()) + " " + LTrim(Str(RecNo())))
SEEK PadR("Seoul", 15)
R("S9_PART_SEOUL", B(Found()) + " " + LTrim(Str(RecNo())))
SEEK PadR("Tokyo", 15)
R("S9_PART_TOKYO", B(Found()) + " " + LTrim(Str(RecNo())))
// S10: Empty table
CLOSE ALL
dbCreate("seek_empty", {{"NAME","C",10,0}})
USE "seek_empty" NEW
INDEX ON FIELD->NAME TO seek_empty_idx
R("S10_RC", LTrim(Str(RecCount())))
SEEK "Test"
R("S10_SEEK", B(Found()) + " " + B(EOF()) + " " + LTrim(Str(RecNo())))
GO TOP
R("S10_TOP", B(EOF()) + " " + LTrim(Str(RecNo())))
// S11: Single record
APPEND BLANK
REPLACE NAME WITH "OnlyOne"
CLOSE ALL
USE "seek_empty" NEW
INDEX ON FIELD->NAME TO seek_empty_idx
SEEK PadR("OnlyOne", 10)
R("S11_FOUND", B(Found()) + " " + LTrim(Str(RecNo())))
SEEK PadR("Other", 10)
R("S11_MISS", B(Found()) + " " + B(EOF()))
SEEK "Only"
R("S11_PARTIAL", B(Found()) + " " + LTrim(Str(RecNo())))
// S12: State after seek
CLOSE ALL
USE "seek_test" NEW
SET INDEX TO seek_name
SEEK PadR("Name_020", 20)
R("S12_SEEK", B(Found()) + " " + LTrim(Str(RecNo())))
R("S12_BOF", B(BOF()))
R("S12_EOF", B(EOF()))
SEEK PadR("Name_999", 20)
R("S12_MISS_FOUND", B(Found()))
R("S12_MISS_EOF", B(EOF()))
GO TOP
R("S12_GOTOP", LTrim(Str(RecNo())) + " " + B(Found()))
// S13: Order switch + seek
SET INDEX TO seek_name
SEEK PadR("Name_001", 20)
R("S13_ORD1", B(Found()) + " " + LTrim(Str(RecNo())))
CLOSE ALL
USE "seek_test" NEW
SET INDEX TO seek_city
SEEK PadR("Tokyo", 15)
R("S13_ORD2", B(Found()) + " " + LTrim(Str(RecNo())))
CLOSE ALL
USE "seek_test" NEW
SET INDEX TO seek_id
SEEK Str(25, 6)
R("S13_ORD3", B(Found()) + " " + LTrim(Str(RecNo())))
// S14: Full traversal count
CLOSE ALL
USE "seek_test" NEW
SET INDEX TO seek_name
GO TOP
nCount := 0
DO WHILE !EOF()
nCount++
SKIP
ENDDO
R("S14_NAME_CNT", LTrim(Str(nCount)))
CLOSE ALL
USE "seek_test" NEW
SET INDEX TO seek_city
GO TOP
nCount := 0
DO WHILE !EOF()
nCount++
SKIP
ENDDO
R("S14_CITY_CNT", LTrim(Str(nCount)))
CLOSE ALL
RECOVER
? "ERROR"
END SEQUENCE
RETURN
FUNCTION B(lVal)
IF lVal
RETURN ".T."
ENDIF
RETURN ".F."
PROCEDURE R(cKey, cVal)
? cKey + "=" + cVal
RETURN

View File

@@ -416,11 +416,15 @@ func (a *DBFArea) Seek(key hbrt.Value, softSeek bool, findLast bool) (bool, erro
return true, nil return true, nil
} }
if softSeek && recNo > 0 && !idx.IsEOF() { if softSeek && !idx.IsEOF() {
a.GoTo(recNo) // Softseek: position at the next higher key
a.FEof = false posRecNo := idx.CurRecNo()
a.SetFound(false) if posRecNo > 0 {
return false, nil a.GoTo(posRecNo)
a.FEof = false
a.SetFound(false)
return false, nil
}
} }
// Not found — go to EOF // Not found — go to EOF
@@ -464,7 +468,12 @@ func (a *DBFArea) GoTopIndexed() error {
a.FEof = true a.FEof = true
return a.GoTo(rc + 1) 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. // 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] idx := a.idxState.indexes[a.idxState.current]
hasScope := a.idxState.scopeTop != nil || a.idxState.scopeBottom != nil hasScope := a.idxState.scopeTop != nil || a.idxState.scopeBottom != nil
setDel := hbrdd.IsSetDeleted != nil && hbrdd.IsSetDeleted()
if count > 0 { if count > 0 {
for i := int64(0); i < count; i++ { for i := int64(0); i < count; i++ {
idx.SkipNext() for {
if idx.IsEOF() || idx.CurRecNo() == 0 { idx.SkipNext()
rc, _ := a.RecCount() if idx.IsEOF() || idx.CurRecNo() == 0 {
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() rc, _ := a.RecCount()
a.GoTo(rc + 1) a.GoTo(rc + 1)
a.FEof = true a.FEof = true
return nil 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 { } 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) // 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 { if idx := strings.Index(fieldName, "->"); idx >= 0 {
fieldName = fieldName[idx+2:] fieldName = strings.TrimSpace(fieldName[idx+2:])
} }
// Simple field name // Simple field name

View File

@@ -334,7 +334,7 @@ func rtlDbSeek(t *hbrt.Thread) {
return return
} }
val := t.Local(1) val := t.Local(1)
softSeek := false softSeek := GetSetSoftSeek() // default: check SET SOFTSEEK
findLast := false findLast := false
if nParams >= 2 && !t.Local(2).IsNil() { if nParams >= 2 && !t.Local(2).IsNil() {
softSeek = t.Local(2).AsBool() softSeek = t.Local(2).AsBool()