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:
@@ -491,23 +491,64 @@ func (g *Generator) emitStmt(stmt ast.Stmt, locals localMap) {
|
|||||||
g.indent--
|
g.indent--
|
||||||
g.writeln("}")
|
g.writeln("}")
|
||||||
case *ast.SetCmd:
|
case *ast.SetCmd:
|
||||||
|
upper := strings.ToUpper(s.Setting)
|
||||||
|
|
||||||
|
// Boolean SET toggles — call RTL Set function, no workarea needed
|
||||||
|
setFuncMap := map[string]string{
|
||||||
|
"DELETED": "SETDELETED",
|
||||||
|
"EXACT": "SETEXACT",
|
||||||
|
"SOFTSEEK": "SETSOFTSEEK",
|
||||||
|
"EXCLUSIVE": "SETEXCLUSIVE",
|
||||||
|
"FIXED": "SETFIXED",
|
||||||
|
"CANCEL": "SETCANCEL",
|
||||||
|
"BELL": "SETBELL",
|
||||||
|
"CONFIRM": "SETCONFIRM",
|
||||||
|
"INSERT": "SETINSERT",
|
||||||
|
"ESCAPE": "SETESCAPE",
|
||||||
|
"WRAP": "SETWRAP",
|
||||||
|
}
|
||||||
|
if funcName, ok := setFuncMap[upper]; ok {
|
||||||
|
onOff := strings.ToUpper(s.Extra)
|
||||||
|
if onOff == "ON" || onOff == "OFF" {
|
||||||
|
val := "true"
|
||||||
|
if onOff == "OFF" {
|
||||||
|
val = "false"
|
||||||
|
}
|
||||||
|
g.writeln(fmt.Sprintf(`t.PushSymbol(t.VM().FindSymbol(%q))`, funcName))
|
||||||
|
g.writeln("t.PushNil()")
|
||||||
|
g.writeln(fmt.Sprintf("t.PushBool(%s)", val))
|
||||||
|
g.writeln("t.Do(1)")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value SET commands — SET DATE/DECIMALS/EPOCH TO expr
|
||||||
|
valueFuncMap := map[string]string{
|
||||||
|
"DATE": "__SETDATEFORMAT",
|
||||||
|
"DECIMALS": "SETDECIMALS",
|
||||||
|
"EPOCH": "SETEPOCH",
|
||||||
|
}
|
||||||
|
if funcName, ok := valueFuncMap[upper]; ok && s.Expr != nil {
|
||||||
|
g.writeln(fmt.Sprintf(`t.PushSymbol(t.VM().FindSymbol(%q))`, funcName))
|
||||||
|
g.writeln("t.PushNil()")
|
||||||
|
g.emitExpr(s.Expr)
|
||||||
|
g.writeln("t.Do(1)")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workarea-specific SET commands
|
||||||
g.writeln("{")
|
g.writeln("{")
|
||||||
g.indent++
|
g.indent++
|
||||||
g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)")
|
g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)")
|
||||||
g.writeln("if area := wa.Current(); area != nil {")
|
g.writeln("if area := wa.Current(); area != nil {")
|
||||||
g.indent++
|
g.indent++
|
||||||
upper := strings.ToUpper(s.Setting)
|
|
||||||
switch upper {
|
switch upper {
|
||||||
case "FILTER":
|
case "FILTER":
|
||||||
if s.Expr != nil {
|
if s.Expr != nil {
|
||||||
g.writeln("if flt, ok := area.(hbrdd.Filterer); ok {")
|
|
||||||
g.indent++
|
|
||||||
g.emitExpr(s.Expr)
|
g.emitExpr(s.Expr)
|
||||||
g.writeln(`flt.SetFilter(t.Pop2().AsString(), nil)`)
|
g.writeln(`area.SetFilter(t.Pop2().AsString(), nil)`)
|
||||||
g.indent--
|
|
||||||
g.writeln("}")
|
|
||||||
} else {
|
} else {
|
||||||
g.writeln("if flt, ok := area.(hbrdd.Filterer); ok { flt.ClearFilter() }")
|
g.writeln("area.ClearFilter()")
|
||||||
}
|
}
|
||||||
case "ORDER":
|
case "ORDER":
|
||||||
if s.Expr != nil {
|
if s.Expr != nil {
|
||||||
|
|||||||
@@ -1706,10 +1706,23 @@ func (p *Parser) parseSet() *ast.SetCmd {
|
|||||||
var extra string
|
var extra string
|
||||||
|
|
||||||
// SET commands: consume everything until end of line.
|
// SET commands: consume everything until end of line.
|
||||||
// Values like "GR+/B, W+/BG" can't be parsed as expressions.
|
// Boolean toggles: SET DELETED ON/OFF, SET EXACT ON/OFF, etc.
|
||||||
// SET FILTER TO is special — the condition IS an expression.
|
// Value settings: SET FILTER TO expr, SET ORDER TO n, SET DATE TO fmt
|
||||||
upperSetting := strings.ToUpper(setting)
|
upperSetting := strings.ToUpper(setting)
|
||||||
if p.match(token.TO) {
|
|
||||||
|
// Check for ON/OFF boolean toggle
|
||||||
|
booleanSets := map[string]bool{
|
||||||
|
"DELETED": true, "EXACT": true, "SOFTSEEK": true, "EXCLUSIVE": true,
|
||||||
|
"FIXED": true, "CANCEL": true, "BELL": true, "CONFIRM": true,
|
||||||
|
"INSERT": true, "ESCAPE": true, "WRAP": true, "INTENSITY": true,
|
||||||
|
"SCOREBOARD": true, "CONSOLE": true, "ALTERNATE": true, "PRINTER": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if booleanSets[upperSetting] {
|
||||||
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
||||||
|
extra = strings.ToUpper(p.expectMethodName().Literal)
|
||||||
|
}
|
||||||
|
} else if p.match(token.TO) {
|
||||||
if upperSetting == "FILTER" || upperSetting == "RELATION" || upperSetting == "ORDER" || upperSetting == "INDEX" {
|
if upperSetting == "FILTER" || upperSetting == "RELATION" || upperSetting == "ORDER" || upperSetting == "INDEX" {
|
||||||
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
||||||
expr = p.parseExpr()
|
expr = p.parseExpr()
|
||||||
@@ -1718,6 +1731,10 @@ func (p *Parser) parseSet() *ast.SetCmd {
|
|||||||
p.advance()
|
p.advance()
|
||||||
extra = p.expectMethodName().Literal
|
extra = p.expectMethodName().Literal
|
||||||
}
|
}
|
||||||
|
} else if upperSetting == "DATE" || upperSetting == "DECIMALS" || upperSetting == "EPOCH" {
|
||||||
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
||||||
|
expr = p.parseExpr()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -241,10 +241,7 @@ func (p *Parser) stmtRecallPackZap() ast.Stmt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) stmtSet() ast.Stmt {
|
func (p *Parser) stmtSet() ast.Stmt {
|
||||||
// SET command — skip to EOL (SET COLOR, SET FILTER, SET ORDER, etc.)
|
return p.parseSet()
|
||||||
p.skipToEndOfLine()
|
|
||||||
p.expectEndOfStmt()
|
|
||||||
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) stmtDefer() ast.Stmt { return p.parseDefer() }
|
func (p *Parser) stmtDefer() ast.Stmt { return p.parseDefer() }
|
||||||
|
|||||||
229
examples/test_search.prg
Normal file
229
examples/test_search.prg
Normal 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
|
||||||
@@ -27,6 +27,10 @@ type BaseArea struct {
|
|||||||
filterExpr string
|
filterExpr string
|
||||||
filterBlock func(*hbrt.Thread) bool
|
filterBlock func(*hbrt.Thread) bool
|
||||||
|
|
||||||
|
// Locate
|
||||||
|
locateExpr string
|
||||||
|
locateBlock func(*hbrt.Thread) bool
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
relations []*Relation
|
relations []*Relation
|
||||||
}
|
}
|
||||||
@@ -120,3 +124,23 @@ func (a *BaseArea) ClearRelation() error {
|
|||||||
a.relations = nil
|
a.relations = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Locate support ---
|
||||||
|
|
||||||
|
func (a *BaseArea) SetLocate(expr string, block func(*hbrt.Thread) bool) {
|
||||||
|
a.locateExpr = expr
|
||||||
|
a.locateBlock = block
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *BaseArea) LocateBlock() func(*hbrt.Thread) bool {
|
||||||
|
return a.locateBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Global RDD configuration callbacks ---
|
||||||
|
// These are set by hbrtl to avoid circular imports.
|
||||||
|
|
||||||
|
var (
|
||||||
|
// IsSetDeleted returns true when SET DELETED is ON.
|
||||||
|
// Set by hbrtl.init() to hbrtl.GetSetDeleted.
|
||||||
|
IsSetDeleted func() bool
|
||||||
|
)
|
||||||
|
|||||||
@@ -481,6 +481,10 @@ func (a *DBFArea) Skip(count int64) error {
|
|||||||
if err := a.GoTo(newRec); err != nil {
|
if err := a.GoTo(newRec); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Skip deleted records when SET DELETED ON
|
||||||
|
if err := a.skipFilter(1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for i := int64(0); i > count; i-- {
|
for i := int64(0); i > count; i-- {
|
||||||
@@ -491,6 +495,9 @@ func (a *DBFArea) Skip(count int64) error {
|
|||||||
if err := a.GoTo(a.recNo - 1); err != nil {
|
if err := a.GoTo(a.recNo - 1); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := a.skipFilter(-1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,9 +713,49 @@ func (a *DBFArea) updateHeader() {
|
|||||||
WriteHeader(a.dataFile, &a.header)
|
WriteHeader(a.dataFile, &a.header)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// skipFilter advances cursor past deleted/filtered records.
|
||||||
|
// Harbour: hb_dbfSkipFilter in dbf1.c
|
||||||
|
// direction: 1 = forward, -1 = backward
|
||||||
func (a *DBFArea) skipFilter(direction int64) error {
|
func (a *DBFArea) skipFilter(direction int64) error {
|
||||||
// TODO: implement SET FILTER and SET DELETED filtering
|
if direction == 0 {
|
||||||
// For now, no filtering applied
|
direction = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
setDel := hbrdd.IsSetDeleted != nil && hbrdd.IsSetDeleted()
|
||||||
|
|
||||||
|
if !setDel {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for !a.FEof && !a.FBof {
|
||||||
|
if !a.Deleted() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to next/prev record
|
||||||
|
if direction > 0 {
|
||||||
|
newRec := a.recNo + 1
|
||||||
|
if newRec > a.recCount {
|
||||||
|
a.FEof = true
|
||||||
|
a.recNo = a.recCount + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err := a.GoTo(newRec); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if a.recNo <= 1 {
|
||||||
|
a.FBof = true
|
||||||
|
if err := a.GoTo(1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err := a.GoTo(a.recNo - 1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,16 @@ type Area interface {
|
|||||||
// Bulk operations
|
// Bulk operations
|
||||||
Pack() error
|
Pack() error
|
||||||
Zap() error
|
Zap() error
|
||||||
|
|
||||||
|
// State — Harbour: hb_waSetFound, locate support
|
||||||
|
SetFound(b bool)
|
||||||
|
SetLocate(expr string, block func(*hbrt.Thread) bool)
|
||||||
|
LocateBlock() func(*hbrt.Thread) bool
|
||||||
|
|
||||||
|
// Filter
|
||||||
|
SetFilter(expr string, block func(*hbrt.Thread) bool) error
|
||||||
|
ClearFilter() error
|
||||||
|
HasFilter() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Optional interfaces (drivers implement as needed) ---
|
// --- Optional interfaces (drivers implement as needed) ---
|
||||||
@@ -177,14 +187,6 @@ type Locker interface {
|
|||||||
IsLocked(recNo uint32) bool
|
IsLocked(recNo uint32) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filterer provides SET FILTER support.
|
|
||||||
// Harbour: SELF_SETFILTER, SELF_CLEARFILTER
|
|
||||||
type Filterer interface {
|
|
||||||
SetFilter(expr string, block func(*hbrt.Thread) bool) error
|
|
||||||
ClearFilter() error
|
|
||||||
HasFilter() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Relater provides SET RELATION support.
|
// Relater provides SET RELATION support.
|
||||||
// Harbour: SELF_SETREL, SELF_CLEARREL, SELF_FORCEREL
|
// Harbour: SELF_SETREL, SELF_CLEARREL, SELF_FORCEREL
|
||||||
type Relater interface {
|
type Relater interface {
|
||||||
|
|||||||
@@ -131,6 +131,12 @@ type memArea struct {
|
|||||||
curIndex int // -1 = natural order, 0+ = index
|
curIndex int // -1 = natural order, 0+ = index
|
||||||
indexPos int // position in current index
|
indexPos int // position in current index
|
||||||
closed bool
|
closed bool
|
||||||
|
|
||||||
|
// Filter/Locate
|
||||||
|
filterExpr string
|
||||||
|
filterBlock func(*hbrt.Thread) bool
|
||||||
|
locateExpr string
|
||||||
|
locateBlock func(*hbrt.Thread) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMemArea(tbl *memTable, alias string, drv *MemDriver) *memArea {
|
func newMemArea(tbl *memTable, alias string, drv *MemDriver) *memArea {
|
||||||
@@ -173,7 +179,26 @@ func (a *memArea) Flush() error { return nil } // no-op: memory only
|
|||||||
|
|
||||||
func (a *memArea) BOF() bool { return a.bof }
|
func (a *memArea) BOF() bool { return a.bof }
|
||||||
func (a *memArea) EOF() bool { return a.eof }
|
func (a *memArea) EOF() bool { return a.eof }
|
||||||
func (a *memArea) Found() bool { return a.found }
|
func (a *memArea) Found() bool { return a.found }
|
||||||
|
func (a *memArea) SetFound(b bool) { a.found = b }
|
||||||
|
|
||||||
|
func (a *memArea) SetLocate(expr string, block func(*hbrt.Thread) bool) {
|
||||||
|
a.locateExpr = expr
|
||||||
|
a.locateBlock = block
|
||||||
|
}
|
||||||
|
func (a *memArea) LocateBlock() func(*hbrt.Thread) bool { return a.locateBlock }
|
||||||
|
|
||||||
|
func (a *memArea) SetFilter(expr string, block func(*hbrt.Thread) bool) error {
|
||||||
|
a.filterExpr = expr
|
||||||
|
a.filterBlock = block
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (a *memArea) ClearFilter() error {
|
||||||
|
a.filterExpr = ""
|
||||||
|
a.filterBlock = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (a *memArea) HasFilter() bool { return a.filterBlock != nil }
|
||||||
|
|
||||||
func (a *memArea) GoTo(recNo uint32) error {
|
func (a *memArea) GoTo(recNo uint32) error {
|
||||||
a.tbl.mu.RLock()
|
a.tbl.mu.RLock()
|
||||||
|
|||||||
@@ -400,3 +400,203 @@ func rtlDbZap(t *hbrt.Thread) {
|
|||||||
}
|
}
|
||||||
t.RetNil()
|
t.RetNil()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- LOCATE / CONTINUE ---
|
||||||
|
// Harbour: DBLOCATE(bFor, bWhile, nNext, nRec, lRest) → .T./.F.
|
||||||
|
// Searches from current position for record matching condition.
|
||||||
|
|
||||||
|
func rtlDbLocate(t *hbrt.Thread) {
|
||||||
|
nParams := t.ParamCount()
|
||||||
|
t.Frame(nParams, 0)
|
||||||
|
defer t.EndProc()
|
||||||
|
|
||||||
|
wam := getWA(t)
|
||||||
|
if wam == nil {
|
||||||
|
t.RetBool(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
area := wam.Current()
|
||||||
|
if area == nil {
|
||||||
|
t.RetBool(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// param 1: bFor condition block
|
||||||
|
var bFor hbrt.Value
|
||||||
|
if nParams >= 1 && !t.Local(1).IsNil() {
|
||||||
|
bFor = t.Local(1)
|
||||||
|
}
|
||||||
|
// param 2: bWhile condition block
|
||||||
|
var bWhile hbrt.Value
|
||||||
|
if nParams >= 2 && !t.Local(2).IsNil() {
|
||||||
|
bWhile = t.Local(2)
|
||||||
|
}
|
||||||
|
// param 3: nNext — max records to scan
|
||||||
|
nNext := int64(0) // 0 = all
|
||||||
|
if nParams >= 3 && !t.Local(3).IsNil() {
|
||||||
|
nNext = t.Local(3).AsNumInt()
|
||||||
|
}
|
||||||
|
// param 4: nRec — specific record number
|
||||||
|
if nParams >= 4 && !t.Local(4).IsNil() {
|
||||||
|
nRec := uint32(t.Local(4).AsNumInt())
|
||||||
|
area.GoTo(nRec)
|
||||||
|
if bFor.IsBlock() {
|
||||||
|
t.PendingParams2(0)
|
||||||
|
bFor.AsBlock().Fn(t)
|
||||||
|
result := t.Pop2()
|
||||||
|
area.SetFound(result.AsBool())
|
||||||
|
} else {
|
||||||
|
area.SetFound(true)
|
||||||
|
}
|
||||||
|
t.RetBool(area.Found())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// param 5: lRest — .T. = continue from current, .F. = from top
|
||||||
|
lRest := false
|
||||||
|
if nParams >= 5 && !t.Local(5).IsNil() {
|
||||||
|
lRest = t.Local(5).AsBool()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !lRest && nNext == 0 {
|
||||||
|
area.GoTop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store locate block for __dbContinue
|
||||||
|
if bFor.IsBlock() {
|
||||||
|
area.SetLocate("", func(lt *hbrt.Thread) bool {
|
||||||
|
lt.PendingParams2(0)
|
||||||
|
bFor.AsBlock().Fn(lt)
|
||||||
|
return lt.Pop2().AsBool()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
count := int64(0)
|
||||||
|
for !area.EOF() {
|
||||||
|
if nNext > 0 && count >= nNext {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Check WHILE condition
|
||||||
|
if bWhile.IsBlock() {
|
||||||
|
t.PendingParams2(0)
|
||||||
|
bWhile.AsBlock().Fn(t)
|
||||||
|
if !t.Pop2().AsBool() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check FOR condition
|
||||||
|
if bFor.IsBlock() {
|
||||||
|
t.PendingParams2(0)
|
||||||
|
bFor.AsBlock().Fn(t)
|
||||||
|
if t.Pop2().AsBool() {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
area.Skip(1)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
area.SetFound(found)
|
||||||
|
t.RetBool(found)
|
||||||
|
}
|
||||||
|
|
||||||
|
// __DBCONTINUE — continue previous LOCATE search.
|
||||||
|
// Harbour: __dbContinue()
|
||||||
|
func rtlDbContinue(t *hbrt.Thread) {
|
||||||
|
t.Frame(0, 0)
|
||||||
|
defer t.EndProc()
|
||||||
|
|
||||||
|
wam := getWA(t)
|
||||||
|
if wam == nil {
|
||||||
|
t.RetBool(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
area := wam.Current()
|
||||||
|
if area == nil {
|
||||||
|
t.RetBool(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
blk := area.LocateBlock()
|
||||||
|
if blk == nil {
|
||||||
|
area.SetFound(false)
|
||||||
|
t.RetBool(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip past current record
|
||||||
|
area.Skip(1)
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for !area.EOF() {
|
||||||
|
if blk(t) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
area.Skip(1)
|
||||||
|
}
|
||||||
|
area.SetFound(found)
|
||||||
|
t.RetBool(found)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DBSETFILTER / DBCLEARFILTER / DBFILTER ---
|
||||||
|
|
||||||
|
// DBSETFILTER(bCondition [, cCondition])
|
||||||
|
func rtlDbSetFilter(t *hbrt.Thread) {
|
||||||
|
nParams := t.ParamCount()
|
||||||
|
t.Frame(nParams, 0)
|
||||||
|
defer t.EndProc()
|
||||||
|
|
||||||
|
wam := getWA(t)
|
||||||
|
if wam == nil {
|
||||||
|
t.RetNil()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
area := wam.Current()
|
||||||
|
if area == nil {
|
||||||
|
t.RetNil()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if nParams >= 1 && t.Local(1).IsBlock() {
|
||||||
|
bFilter := t.Local(1)
|
||||||
|
expr := ""
|
||||||
|
if nParams >= 2 && t.Local(2).IsString() {
|
||||||
|
expr = t.Local(2).AsString()
|
||||||
|
}
|
||||||
|
area.SetFilter(expr, func(lt *hbrt.Thread) bool {
|
||||||
|
lt.PendingParams2(0)
|
||||||
|
bFilter.AsBlock().Fn(lt)
|
||||||
|
return lt.Pop2().AsBool()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
t.RetNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DBCLEARFILTER()
|
||||||
|
func rtlDbClearFilter(t *hbrt.Thread) {
|
||||||
|
t.Frame(0, 0)
|
||||||
|
defer t.EndProc()
|
||||||
|
|
||||||
|
wam := getWA(t)
|
||||||
|
if wam == nil {
|
||||||
|
t.RetNil()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if area := wam.Current(); area != nil {
|
||||||
|
area.ClearFilter()
|
||||||
|
}
|
||||||
|
t.RetNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DBFILTER() → cFilterExpression
|
||||||
|
func rtlDbFilter(t *hbrt.Thread) {
|
||||||
|
t.Frame(0, 0)
|
||||||
|
defer t.EndProc()
|
||||||
|
|
||||||
|
// TODO: return stored filter expression from area
|
||||||
|
t.RetString("")
|
||||||
|
}
|
||||||
|
|||||||
@@ -179,6 +179,14 @@ func RegisterRTL(vm *hbrt.VM) {
|
|||||||
hbrt.Sym("PACK", hbrt.FsPublic, rtlDbPack),
|
hbrt.Sym("PACK", hbrt.FsPublic, rtlDbPack),
|
||||||
hbrt.Sym("ZAP", hbrt.FsPublic, rtlDbZap),
|
hbrt.Sym("ZAP", hbrt.FsPublic, rtlDbZap),
|
||||||
|
|
||||||
|
// Locate / Filter
|
||||||
|
hbrt.Sym("DBLOCATE", hbrt.FsPublic, rtlDbLocate),
|
||||||
|
hbrt.Sym("__DBLOCATE", hbrt.FsPublic, rtlDbLocate),
|
||||||
|
hbrt.Sym("__DBCONTINUE", hbrt.FsPublic, rtlDbContinue),
|
||||||
|
hbrt.Sym("DBSETFILTER", hbrt.FsPublic, rtlDbSetFilter),
|
||||||
|
hbrt.Sym("DBCLEARFILTER", hbrt.FsPublic, rtlDbClearFilter),
|
||||||
|
hbrt.Sym("DBFILTER", hbrt.FsPublic, rtlDbFilter),
|
||||||
|
|
||||||
// Encoding / Hashing (Go stdlib)
|
// Encoding / Hashing (Go stdlib)
|
||||||
hbrt.Sym("HB_MD5", hbrt.FsPublic, HbMD5),
|
hbrt.Sym("HB_MD5", hbrt.FsPublic, HbMD5),
|
||||||
hbrt.Sym("HB_SHA256", hbrt.FsPublic, HbSHA256),
|
hbrt.Sym("HB_SHA256", hbrt.FsPublic, HbSHA256),
|
||||||
@@ -247,6 +255,18 @@ func RegisterRTL(vm *hbrt.VM) {
|
|||||||
hbrt.Sym("__SETDATEFORMAT", hbrt.FsPublic, SetDateFunc),
|
hbrt.Sym("__SETDATEFORMAT", hbrt.FsPublic, SetDateFunc),
|
||||||
hbrt.Sym("__SETDECIMALS", hbrt.FsPublic, SetDecimalsFunc),
|
hbrt.Sym("__SETDECIMALS", hbrt.FsPublic, SetDecimalsFunc),
|
||||||
hbrt.Sym("__SETEPOCH", hbrt.FsPublic, SetEpochFunc),
|
hbrt.Sym("__SETEPOCH", hbrt.FsPublic, SetEpochFunc),
|
||||||
|
// SET toggle functions
|
||||||
|
hbrt.Sym("SETDELETED", hbrt.FsPublic, SetDeletedFunc),
|
||||||
|
hbrt.Sym("SETEXACT", hbrt.FsPublic, SetExactFunc),
|
||||||
|
hbrt.Sym("SETSOFTSEEK", hbrt.FsPublic, SetSoftSeekFunc),
|
||||||
|
hbrt.Sym("SETEXCLUSIVE", hbrt.FsPublic, SetExclusiveFunc),
|
||||||
|
hbrt.Sym("SETFIXED", hbrt.FsPublic, SetFixedFunc),
|
||||||
|
hbrt.Sym("SETCANCEL", hbrt.FsPublic, SetCancelFunc),
|
||||||
|
hbrt.Sym("SETBELL", hbrt.FsPublic, SetBellFunc),
|
||||||
|
hbrt.Sym("SETCONFIRM", hbrt.FsPublic, SetConfirmFunc),
|
||||||
|
hbrt.Sym("SETINSERT", hbrt.FsPublic, SetInsertFunc),
|
||||||
|
hbrt.Sym("SETESCAPE", hbrt.FsPublic, SetEscapeFunc),
|
||||||
|
hbrt.Sym("SETWRAP", hbrt.FsPublic, SetWrapFunc),
|
||||||
// SET constants
|
// SET constants
|
||||||
hbrt.Sym("_SET_EXACT", hbrt.FsPublic, SetConstExact),
|
hbrt.Sym("_SET_EXACT", hbrt.FsPublic, SetConstExact),
|
||||||
hbrt.Sym("_SET_DELETED", hbrt.FsPublic, SetConstDeleted),
|
hbrt.Sym("_SET_DELETED", hbrt.FsPublic, SetConstDeleted),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
package hbrtl
|
package hbrtl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"five/hbrdd"
|
||||||
"five/hbrt"
|
"five/hbrt"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@@ -261,8 +262,10 @@ func SetConstDateFmt(t *hbrt.Thread) { t.Frame(0, 0); defer t.EndProc(); t.Pus
|
|||||||
func SetConstDecimals(t *hbrt.Thread) { t.Frame(0, 0); defer t.EndProc(); t.PushInt(SetDecimals); t.RetValue() }
|
func SetConstDecimals(t *hbrt.Thread) { t.Frame(0, 0); defer t.EndProc(); t.PushInt(SetDecimals); t.RetValue() }
|
||||||
func SetConstEpoch(t *hbrt.Thread) { t.Frame(0, 0); defer t.EndProc(); t.PushInt(SetEpoch); t.RetValue() }
|
func SetConstEpoch(t *hbrt.Thread) { t.Frame(0, 0); defer t.EndProc(); t.PushInt(SetEpoch); t.RetValue() }
|
||||||
|
|
||||||
// init registers default DATE format and EPOCH
|
// init registers default DATE format and EPOCH, and hooks into hbrdd.
|
||||||
func init() {
|
func init() {
|
||||||
|
hbrdd.IsSetDeleted = GetSetDeleted
|
||||||
|
|
||||||
setMu.Lock()
|
setMu.Lock()
|
||||||
if _, ok := settings[SetDateFmt]; !ok {
|
if _, ok := settings[SetDateFmt]; !ok {
|
||||||
settings[SetDateFmt] = hbrt.MakeString("mm/dd/yy")
|
settings[SetDateFmt] = hbrt.MakeString("mm/dd/yy")
|
||||||
|
|||||||
@@ -253,28 +253,44 @@ func Space(t *hbrt.Thread) {
|
|||||||
|
|
||||||
// PadR pads a string on the right to a specified length.
|
// PadR pads a string on the right to a specified length.
|
||||||
func PadR(t *hbrt.Thread) {
|
func PadR(t *hbrt.Thread) {
|
||||||
t.Frame(2, 0)
|
nParams := t.ParamCount()
|
||||||
|
t.Frame(nParams, 0)
|
||||||
defer t.EndProc()
|
defer t.EndProc()
|
||||||
s := valueToDisplay(t.Local(1))
|
s := valueToDisplay(t.Local(1))
|
||||||
n := int(t.Local(2).AsNumInt())
|
n := int(t.Local(2).AsNumInt())
|
||||||
|
fill := " "
|
||||||
|
if nParams >= 3 && t.Local(3).IsString() {
|
||||||
|
f := t.Local(3).AsString()
|
||||||
|
if len(f) > 0 {
|
||||||
|
fill = f[:1]
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(s) >= n {
|
if len(s) >= n {
|
||||||
t.PushString(s[:n])
|
t.PushString(s[:n])
|
||||||
} else {
|
} else {
|
||||||
t.PushString(s + strings.Repeat(" ", n-len(s)))
|
t.PushString(s + strings.Repeat(fill, n-len(s)))
|
||||||
}
|
}
|
||||||
t.RetValue()
|
t.RetValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
// PadL pads a string on the left to a specified length.
|
// PadL pads a string on the left to a specified length.
|
||||||
func PadL(t *hbrt.Thread) {
|
func PadL(t *hbrt.Thread) {
|
||||||
t.Frame(2, 0)
|
nParams := t.ParamCount()
|
||||||
|
t.Frame(nParams, 0)
|
||||||
defer t.EndProc()
|
defer t.EndProc()
|
||||||
s := valueToDisplay(t.Local(1))
|
s := valueToDisplay(t.Local(1))
|
||||||
n := int(t.Local(2).AsNumInt())
|
n := int(t.Local(2).AsNumInt())
|
||||||
|
fill := " "
|
||||||
|
if nParams >= 3 && t.Local(3).IsString() {
|
||||||
|
f := t.Local(3).AsString()
|
||||||
|
if len(f) > 0 {
|
||||||
|
fill = f[:1]
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(s) >= n {
|
if len(s) >= n {
|
||||||
t.PushString(s[len(s)-n:])
|
t.PushString(s[len(s)-n:])
|
||||||
} else {
|
} else {
|
||||||
t.PushString(strings.Repeat(" ", n-len(s)) + s)
|
t.PushString(strings.Repeat(fill, n-len(s)) + s)
|
||||||
}
|
}
|
||||||
t.RetValue()
|
t.RetValue()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user