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

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