feat: SET DELETED filtering, SEEK/LOCATE/CONTINUE, SET command codegen

- skipFilter: skip deleted records in GoTop/GoBottom/Skip when SET DELETED ON
- hbrdd.IsSetDeleted callback: avoids circular import hbrdd→hbrtl
- Parser: capture ON/OFF for boolean SET commands (DELETED, EXACT, SOFTSEEK, etc.)
- Parser: capture TO expr for SET DATE/DECIMALS/EPOCH
- Gengo: emit proper t.Do() calls for 11 SET toggles + 3 value SETs
- stmtSet: was stub (skipToEOL), now calls parseSet()
- RTL: register 11 SET toggle functions (SETDELETED, SETEXACT, etc.)
- RTL: DBLOCATE/DBCONTINUE for sequential search
- RTL: DBSETFILTER/DBCLEARFILTER/DBFILTER
- PadL/PadR: support 3rd param fill character
- Area interface: added SetFound, SetLocate, LocateBlock, filter methods
- MemRDD: implements new Area interface methods
- Comprehensive PRG test: test_search.prg (7 test suites all pass)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 22:33:59 +09:00
parent 08c0ef13d4
commit 21fd9dc65c
12 changed files with 651 additions and 30 deletions

229
examples/test_search.prg Normal file
View File

@@ -0,0 +1,229 @@
// Comprehensive search test: SEEK, SOFTSEEK, SET DELETED, LOCATE/CONTINUE
// Tests all search-related RDD functionality end-to-end.
FUNCTION Main()
LOCAL i, aStruct, nCount
? "=========================================="
? " Five Search/Filter Test Suite"
? "=========================================="
?
// --- Setup: Create test table ---
aStruct := {{"ID","N",6,0}, {"NAME","C",20,0}, {"CITY","C",15,0}, {"AGE","N",3,0}, {"ACTIVE","L",1,0}}
dbCreate("search_test", aStruct)
USE "search_test"
// Insert 50 records
FOR i := 1 TO 50
APPEND BLANK
FieldPut(1, i)
FieldPut(2, "Name_" + PadL(LTrim(Str(i)), 3, "0"))
IF i % 5 == 0
FieldPut(3, "Seoul")
ELSEIF i % 5 == 1
FieldPut(3, "Tokyo")
ELSEIF i % 5 == 2
FieldPut(3, "Beijing")
ELSEIF i % 5 == 3
FieldPut(3, "London")
ELSE
FieldPut(3, "NewYork")
ENDIF
FieldPut(4, 20 + (i % 40))
FieldPut(5, (i % 3 != 0))
NEXT
? "Setup: 50 records created, RecCount =", RecCount()
?
// ============================================
// TEST 1: INDEX + SEEK (exact match)
// ============================================
? "--- TEST 1: INDEX + SEEK ---"
INDEX ON NAME TO search_name
? "Index on NAME created."
SEEK "Name_001"
? "SEEK 'Name_001': Found =", Found(), "RecNo =", RecNo()
IF !Found()
? " *** FAIL: should find Name_001"
ENDIF
SEEK "Name_025"
? "SEEK 'Name_025': Found =", Found(), "RecNo =", RecNo()
IF !Found()
? " *** FAIL: should find Name_025"
ENDIF
SEEK "Name_050"
? "SEEK 'Name_050': Found =", Found(), "RecNo =", RecNo()
IF !Found()
? " *** FAIL: should find Name_050"
ENDIF
SEEK "Name_999"
? "SEEK 'Name_999': Found =", Found(), "Eof =", Eof()
IF Found()
? " *** FAIL: should NOT find Name_999"
ENDIF
?
// ============================================
// TEST 2: SOFTSEEK
// ============================================
? "--- TEST 2: SOFTSEEK ---"
SET SOFTSEEK ON
SEEK "Name_030"
? "SOFTSEEK 'Name_030': Found =", Found(), "RecNo =", RecNo()
IF Found()
? " Name =", FieldGet(2)
ENDIF
SEEK "Name_031"
? "SOFTSEEK 'Name_031': Found =", Found(), "RecNo =", RecNo()
IF !Found() .AND. !Eof()
? " Positioned at:", FieldGet(2), "(softseek OK)"
ENDIF
SET SOFTSEEK OFF
?
// ============================================
// TEST 3: Partial key SEEK
// ============================================
? "--- TEST 3: Partial Key SEEK ---"
SEEK "Name_01"
? "SEEK 'Name_01' (partial): Found =", Found()
IF Found()
? " Name =", FieldGet(2), "(should be Name_010 or similar)"
ENDIF
?
// ============================================
// TEST 4: SET DELETED — skip deleted records
// ============================================
? "--- TEST 4: SET DELETED ---"
// Delete records 10,20,30,40,50 (every 10th)
SET ORDER TO 0 // natural order
GO TOP
FOR i := 1 TO 50
GO i
IF FieldGet(1) % 10 == 0
DELETE
ENDIF
NEXT
? "Deleted records: 10, 20, 30, 40, 50"
SET DELETED OFF
GO TOP
nCount := 0
DO WHILE !Eof()
nCount++
SKIP
ENDDO
? "SET DELETED OFF: visible =", nCount, "(should be 50)"
SET DELETED ON
GO TOP
nCount := 0
DO WHILE !Eof()
nCount++
SKIP
ENDDO
? "SET DELETED ON: visible =", nCount, "(should be 45)"
IF nCount != 45
? " *** FAIL: expected 45, got", nCount
ELSE
? " PASS"
ENDIF
// GoTop should skip deleted record if record 1 would be first
// Let's delete record 1 and test
GO 1
DELETE
SET DELETED ON
GO TOP
? "After deleting rec 1, GO TOP recNo =", RecNo(), "(should be 2)"
IF RecNo() != 2
? " *** FAIL: GoTop should skip deleted rec 1"
ELSE
? " PASS"
ENDIF
// Recall record 1 for subsequent tests
GO 1
RECALL
SET DELETED OFF
?
// ============================================
// TEST 5: SET DELETED + INDEX
// ============================================
? "--- TEST 5: SET DELETED + INDEX ---"
SET ORDER TO 1 // NAME index
SET DELETED ON
GO TOP
nCount := 0
DO WHILE !Eof()
nCount++
SKIP
ENDDO
? "Indexed + SET DELETED ON: visible =", nCount, "(should be 45)"
IF nCount != 45
? " *** FAIL"
ELSE
? " PASS"
ENDIF
SET DELETED OFF
?
// ============================================
// TEST 6: Navigation correctness
// ============================================
? "--- TEST 6: Navigation ---"
SET ORDER TO 0 // natural
GO TOP
? "GO TOP: RecNo =", RecNo(), "(should be 1)"
GO BOTTOM
? "GO BOTTOM: RecNo =", RecNo(), "(should be 50)"
SKIP -1
? "SKIP -1: RecNo =", RecNo(), "(should be 49)"
GO TOP
SKIP 5
? "GO TOP + SKIP 5: RecNo =", RecNo(), "(should be 6)"
?
// ============================================
// TEST 7: SEEK on city index
// ============================================
? "--- TEST 7: Second index ---"
INDEX ON CITY TO search_city
? "Index on CITY created."
SEEK "Seoul"
? "SEEK 'Seoul': Found =", Found()
IF Found()
? " City =", FieldGet(3), "ID =", FieldGet(1)
ENDIF
SEEK "Tokyo"
? "SEEK 'Tokyo': Found =", Found()
IF Found()
? " City =", FieldGet(3), "ID =", FieldGet(1)
ENDIF
SEEK "Paris"
? "SEEK 'Paris': Found =", Found(), "(should be .F.)"
?
// ============================================
// Cleanup
// ============================================
CLOSE ALL
? "=========================================="
? " All search tests complete."
? "=========================================="
RETURN