From b7028791d64b245900de30144ed2aa3b5132e7e8 Mon Sep 17 00:00:00 2001 From: Charles KWON OhJun Date: Mon, 6 Apr 2026 14:08:51 +0900 Subject: [PATCH] =?UTF-8?q?fix:=205=20seek/dbf=20bugs=20=E2=80=94=2077/77?= =?UTF-8?q?=20thorough=20Harbour=20compatibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- compiler/gengo/gen_cmd.go | 7 +- compiler/gengo/gen_util.go | 48 +++++- compiler/gengo/gengo.go | 9 +- examples/seek_thorough.prg | 297 +++++++++++++++++++++++++++++++++++++ hbrdd/dbf/indexer.go | 57 ++++--- hbrtl/database.go | 2 +- 6 files changed, 393 insertions(+), 27 deletions(-) create mode 100644 examples/seek_thorough.prg diff --git a/compiler/gengo/gen_cmd.go b/compiler/gengo/gen_cmd.go index a6c76fd..85dbaa9 100644 --- a/compiler/gengo/gen_cmd.go +++ b/compiler/gengo/gen_cmd.go @@ -98,7 +98,12 @@ func (g *Generator) emitSeekCmd(s *ast.SeekCmd, locals localMap) { g.writeln("_key := t.Pop2()") g.writeln("if _idx, ok := area.(hbrdd.Indexer); ok {") 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.indent-- g.writeln("}") diff --git a/compiler/gengo/gen_util.go b/compiler/gengo/gen_util.go index 6ba34f9..f4cfb4c 100644 --- a/compiler/gengo/gen_util.go +++ b/compiler/gengo/gen_util.go @@ -12,13 +12,51 @@ func hasXBaseCommands(file *ast.File) bool { if !ok { continue } - for _, s := range fn.Body { - switch s.(type) { - case *ast.UseCmd, *ast.GoCmd, *ast.SkipCmd, *ast.SeekCmd, - *ast.ReplaceCmd, *ast.AppendCmd, *ast.DeleteCmd, - *ast.SelectCmd, *ast.IndexCmd, *ast.SetCmd: + if scanStmtsForXBase(fn.Body) { + return true + } + } + 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 } + 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 diff --git a/compiler/gengo/gengo.go b/compiler/gengo/gengo.go index 3d9b977..19f5aa1 100644 --- a/compiler/gengo/gengo.go +++ b/compiler/gengo/gengo.go @@ -564,10 +564,15 @@ func (g *Generator) emitStmt(stmt ast.Stmt, locals localMap) { } case "INDEX": if s.Expr != nil { + fileStr := exprToString(s.Expr) g.writeln("if idx, ok := area.(hbrdd.Indexer); ok {") g.indent++ - g.emitExpr(s.Expr) - g.writeln(`idx.OrderListAdd(t.Pop2().AsString())`) + if fileStr != "" { + g.writeln(fmt.Sprintf(`idx.OrderListAdd(%q)`, fileStr)) + } else { + g.emitExpr(s.Expr) + g.writeln(`idx.OrderListAdd(t.Pop2().AsString())`) + } g.indent-- g.writeln("}") } else { diff --git a/examples/seek_thorough.prg b/examples/seek_thorough.prg new file mode 100644 index 0000000..b8590bb --- /dev/null +++ b/examples/seek_thorough.prg @@ -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 diff --git a/hbrdd/dbf/indexer.go b/hbrdd/dbf/indexer.go index fa272a8..c60fdc4 100644 --- a/hbrdd/dbf/indexer.go +++ b/hbrdd/dbf/indexer.go @@ -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 diff --git a/hbrtl/database.go b/hbrtl/database.go index 6f77fd8..e54480c 100644 --- a/hbrtl/database.go +++ b/hbrtl/database.go @@ -334,7 +334,7 @@ func rtlDbSeek(t *hbrt.Thread) { return } val := t.Local(1) - softSeek := false + softSeek := GetSetSoftSeek() // default: check SET SOFTSEEK findLast := false if nParams >= 2 && !t.Local(2).IsNil() { softSeek = t.Local(2).AsBool()