diff --git a/docs/five-syntax-en.md b/docs/five-syntax-en.md index 838a2f4..dc6cbe3 100644 --- a/docs/five-syntax-en.md +++ b/docs/five-syntax-en.md @@ -423,6 +423,248 @@ PROCEDURE Main() | Constants | No | Pi, E, Phi, Ln2, Sqrt2 | | Special values | No | NaN, Inf, MaxFloat64 | +## Value Type Methods (Five Extension) + +Five provides 52 built-in methods on basic types. All support chaining: + +### String Methods (20) + +```prg +LOCAL cStr := " Hello World " +? cStr:Trim() // "Hello World" +? cStr:Upper() // " HELLO WORLD " +? cStr:Lower() // " hello world " +? cStr:Left(7) // " Hello" +? cStr:Right(7) // "orld " +? cStr:SubStr(3, 5) // "Hello" +? cStr:At("World") // 9 +? cStr:Len() // 15 +? cStr:Replicate(2) // " Hello World Hello World " +? cStr:Reverse() // " dlroW olleH " +? cStr:IsAlpha() // .F. (starts with space) +? cStr:IsDigit() // .F. +? cStr:IsEmpty() // .F. +? cStr:Trim():Upper():Left(5) // "HELLO" — chaining +``` + +### Array Methods (14) + +```prg +LOCAL aList := {3, 1, 4, 1, 5} +? aList:Len() // 5 +? aList:Sort() // {1, 1, 3, 4, 5} +? aList:Find(4) // 3 (1-based) +? aList:Push(9) // {1,1,3,4,5,9} +? aList:Pop() // 9 +? aList:First() // 1 +? aList:Last() // 5 +? aList:Join(",") // "1,1,3,4,5" +? aList:Reverse() // {5,4,3,1,1} +? aList:Unique() // {5,4,3,1} +? aList:Slice(2, 4) // {4,3} + +// Map/Filter/Each with code blocks +LOCAL aDoubled := {1,2,3}:Map({|x| x * 2}) // {2,4,6} +LOCAL aEven := {1,2,3,4}:Filter({|x| x % 2 == 0}) // {2,4} +{1,2,3}:Each({|x| QOut(x)}) // prints each element +``` + +### Numeric Methods (6) + +```prg +LOCAL nVal := 3.14159 +? nVal:Round(2) // 3.14 +? nVal:Abs() // 3.14159 +? nVal:Int() // 3 +? nVal:Str(10, 4) // " 3.1416" +? nVal:IsZero() // .F. +? (-5):Abs() // 5 +``` + +### Hash Methods (7) + +```prg +LOCAL hData := {"name" => "Charles", "age" => 30} +? hData:Keys() // {"name","age"} +? hData:Values() // {"Charles",30} +? hData:Len() // 2 +? hData:HasKey("name") // .T. +? hData:Remove("age") // {"name" => "Charles"} +? hData:Merge({"city" => "Seoul"}) +``` + +### Any Type Methods (5) + +```prg +LOCAL xVal := "hello" +? xVal:Type() // "C" +? xVal:Clone() // deep copy +? xVal:IsNil() // .F. +? xVal:ToString() // "hello" +? xVal:ValType() // "C" +``` + +## MEMVAR — PUBLIC/PRIVATE Variables + +Harbour-compatible memory variable system. PUBLIC is global, PRIVATE is function-scoped with shadowing: + +```prg +// PUBLIC — accessible throughout the entire program +PUBLIC gAppName +gAppName := "Five Application" + +PROCEDURE Main() + LOCAL cLocal := "local only" + + // PRIVATE — accessible in current function + callees, restored on return + PRIVATE nTemp := 100 + SubFunc() + ? nTemp // 100 (SubFunc's PRIVATE restored) + + ? gAppName // "Five Application" (PUBLIC) +RETURN + +PROCEDURE SubFunc() + PRIVATE nTemp := 999 // shadows caller's nTemp + ? nTemp // 999 +RETURN // nTemp restored to 100 +``` + +### MEMVAR Scope Rules + +| Type | Lifetime | Visibility | Shadowing | +|------|----------|------------|-----------| +| PUBLIC | Until program exit | Everywhere | Can be shadowed by PRIVATE | +| PRIVATE | Until declaring function returns | Declaring function + callees | Nested PRIVATE supported | +| LOCAL | Until declaring function returns | Declaring function only | Independent of MEMVAR | +| STATIC | Until program exit | Declaring function only | Independent of MEMVAR | + +### MEMVAR Access via Macro + +```prg +PUBLIC cName := "Charles" +LOCAL cVar := "cName" +? &cVar // "Charles" — macro searches MEMVAR +``` + +## SET Command System + +Harbour-compatible SET settings. 47+ settings supported: + +```prg +// Boolean toggles +SET EXACT ON // exact string comparison +SET DELETED ON // hide deleted records +SET SOFTSEEK ON // nearest record on failed SEEK +SET EXCLUSIVE OFF // shared mode +SET CONFIRM ON // require confirmation on GET + +// Value settings +SET DATE FORMAT "yyyy-mm-dd" // date format +SET DECIMALS TO 4 // decimal places +SET EPOCH TO 2000 // 2-digit year interpretation base + +// Programmatic access via SET() function +LOCAL lOld := SET(_SET_EXACT, .T.) // set and return previous value +? SET(_SET_EXACT) // .T. +``` + +### SET Constants + +```prg +_SET_EXACT // 1 exact string comparison +_SET_FIXED // 2 fixed decimal point +_SET_DECIMALS // 3 decimal places +_SET_DATEFORMAT // 4 date format +_SET_EPOCH // 5 epoch year +_SET_DELETED // 8 deleted record filter +_SET_EXCLUSIVE // 11 exclusive mode +_SET_SOFTSEEK // 12 soft seek +``` + +## ErrorBlock / Break — Error Handling + +Harbour-compatible structured error handling: + +### BEGIN SEQUENCE / RECOVER + +```prg +LOCAL bOldError +LOCAL oErr + +// Set error handler +bOldError := ErrorBlock({|e| Break(e)}) + +BEGIN SEQUENCE + // Code that may generate an error + USE "nonexistent.dbf" +RECOVER USING oErr + // oErr is an error object (Hash) + ? oErr["DESCRIPTION"] // error description + ? oErr["OPERATION"] // failed operation + ? oErr["SUBSYSTEM"] // subsystem name + ? oErr["GENCODE"] // generic error code +END SEQUENCE + +// Restore previous handler +ErrorBlock(bOldError) +``` + +### ErrorBlock + +```prg +// Get current error handler +LOCAL bHandler := ErrorBlock() + +// Set new handler (returns previous) +LOCAL bOld := ErrorBlock({|e| MyErrorHandler(e)}) + +FUNCTION MyErrorHandler(oErr) + ? "Error:", oErr["DESCRIPTION"] + ? "Operation:", oErr["OPERATION"] + BREAK oErr // pass to RECOVER in BEGIN SEQUENCE +RETURN NIL +``` + +### ErrorNew + +```prg +LOCAL oErr := ErrorNew() +oErr["SUBSYSTEM"] := "MYAPP" +oErr["DESCRIPTION"] := "Custom error" +oErr["OPERATION"] := "MyFunc" +oErr["GENCODE"] := 1001 +oErr["SEVERITY"] := 2 // ES_ERROR +``` + +## MEMO Fields — Transparent Read/Write + +Five handles DBF MEMO fields transparently. +FPT files are automatically created and opened: + +```prg +// Create table with MEMO field — FPT auto-created +USE "notes" NEW +APPEND BLANK +REPLACE NAME WITH "Charles" +REPLACE NOTES WITH "This is a long memo text..." // auto-writes to FPT +? NOTES // "This is a long memo text..." — auto-reads from FPT + +// Large memos work seamlessly +REPLACE NOTES WITH REPLICATE("Large data. ", 1000) // ~12KB +? LEN(NOTES) // 12000 +``` + +### MEMO Internal Behavior + +| Action | Automatic Handling | +|--------|-------------------| +| Create DBF (with M field) | FPT file auto-created | +| Open DBF (with M field) | FPT file auto-opened | +| REPLACE memo WITH text | Write to FPT, store block number in DBF | +| ? memo | Read FPT by block number, return string | +| Close DBF | FPT auto-closed | + ## Example Files | File | Description | diff --git a/docs/five-syntax-ko.md b/docs/five-syntax-ko.md index 0a5f0e6..f955ba7 100644 --- a/docs/five-syntax-ko.md +++ b/docs/five-syntax-ko.md @@ -394,6 +394,248 @@ PROCEDURE Main() | 삼각함수 | 없음 | Sin, Cos, Tan, ... | | 상수 | 없음 | Pi, E, Phi, ... | +## Value 타입 메서드 (Five 확장) + +Five는 기본 타입에 52개의 내장 메서드를 지원합니다. 체이닝 가능: + +### String 메서드 (20개) + +```prg +LOCAL cStr := " Hello World " +? cStr:Trim() // "Hello World" +? cStr:Upper() // " HELLO WORLD " +? cStr:Lower() // " hello world " +? cStr:Left(7) // " Hello" +? cStr:Right(7) // "orld " +? cStr:SubStr(3, 5) // "Hello" +? cStr:At("World") // 9 +? cStr:Len() // 15 +? cStr:Replicate(2) // " Hello World Hello World " +? cStr:Reverse() // " dlroW olleH " +? cStr:IsAlpha() // .F. (starts with space) +? cStr:IsDigit() // .F. +? cStr:IsEmpty() // .F. +? cStr:Trim():Upper():Left(5) // "HELLO" — chaining +``` + +### Array 메서드 (14개) + +```prg +LOCAL aList := {3, 1, 4, 1, 5} +? aList:Len() // 5 +? aList:Sort() // {1, 1, 3, 4, 5} +? aList:Find(4) // 3 (1-based) +? aList:Push(9) // {1,1,3,4,5,9} +? aList:Pop() // 9 +? aList:First() // 1 +? aList:Last() // 5 +? aList:Join(",") // "1,1,3,4,5" +? aList:Reverse() // {5,4,3,1,1} +? aList:Unique() // {5,4,3,1} +? aList:Slice(2, 4) // {4,3} + +// Map/Filter/Each with code blocks +LOCAL aDoubled := {1,2,3}:Map({|x| x * 2}) // {2,4,6} +LOCAL aEven := {1,2,3,4}:Filter({|x| x % 2 == 0}) // {2,4} +{1,2,3}:Each({|x| QOut(x)}) // prints each +``` + +### Numeric 메서드 (6개) + +```prg +LOCAL nVal := 3.14159 +? nVal:Round(2) // 3.14 +? nVal:Abs() // 3.14159 +? nVal:Int() // 3 +? nVal:Str(10, 4) // " 3.1416" +? nVal:IsZero() // .F. +? (-5):Abs() // 5 +``` + +### Hash 메서드 (7개) + +```prg +LOCAL hData := {"name" => "Charles", "age" => 30} +? hData:Keys() // {"name","age"} +? hData:Values() // {"Charles",30} +? hData:Len() // 2 +? hData:HasKey("name") // .T. +? hData:Remove("age") // {"name" => "Charles"} +? hData:Merge({"city" => "Seoul"}) +``` + +### Any 타입 메서드 (5개) + +```prg +LOCAL xVal := "hello" +? xVal:Type() // "C" +? xVal:Clone() // deep copy +? xVal:IsNil() // .F. +? xVal:ToString() // "hello" +? xVal:ValType() // "C" +``` + +## MEMVAR — PUBLIC/PRIVATE 변수 + +Harbour 호환 메모리 변수 시스템. PUBLIC은 전역, PRIVATE은 함수 스코프. + +```prg +// PUBLIC — 프로그램 전체에서 접근 +PUBLIC gAppName +gAppName := "Five Application" + +PROCEDURE Main() + LOCAL cLocal := "local only" + + // PRIVATE — 현재 함수 + 하위 함수에서 접근, 리턴 시 복원 + PRIVATE nTemp := 100 + SubFunc() + ? nTemp // 100 (SubFunc의 PRIVATE이 복원됨) + + ? gAppName // "Five Application" (PUBLIC) +RETURN + +PROCEDURE SubFunc() + PRIVATE nTemp := 999 // shadows caller's nTemp + ? nTemp // 999 +RETURN // nTemp restored to 100 +``` + +### MEMVAR 스코프 규칙 + +| 종류 | 수명 | 가시성 | 섀도잉 | +|------|------|--------|--------| +| PUBLIC | 프로그램 종료까지 | 전체 | PRIVATE이 숨길 수 있음 | +| PRIVATE | 선언 함수 리턴까지 | 선언 함수 + 하위 | 중첩 PRIVATE 가능 | +| LOCAL | 선언 함수 리턴까지 | 선언 함수만 | MEMVAR와 독립 | +| STATIC | 프로그램 종료까지 | 선언 함수만 | MEMVAR와 독립 | + +### 매크로에서 MEMVAR 접근 + +```prg +PUBLIC cName := "Charles" +LOCAL cVar := "cName" +? &cVar // "Charles" — 매크로가 MEMVAR 검색 +``` + +## SET 명령어 시스템 + +Harbour 호환 SET 설정. 47+ 설정 지원: + +```prg +// Boolean 토글 +SET EXACT ON // 문자열 완전 일치 비교 +SET DELETED ON // 삭제 레코드 숨김 +SET SOFTSEEK ON // SEEK 실패 시 가장 가까운 레코드 +SET EXCLUSIVE OFF // 공유 모드 +SET CONFIRM ON // GET 입력 시 확인 필요 + +// 값 설정 +SET DATE FORMAT "yyyy-mm-dd" // 날짜 형식 +SET DECIMALS TO 4 // 소수점 자릿수 +SET EPOCH TO 2000 // 2자리 년도 해석 기준 + +// SET() 함수로 프로그래밍 방식 접근 +LOCAL lOld := SET(_SET_EXACT, .T.) // 설정하고 이전 값 반환 +? SET(_SET_EXACT) // .T. +``` + +### SET 상수 + +```prg +_SET_EXACT // 1 문자열 정확 비교 +_SET_FIXED // 2 고정 소수점 +_SET_DECIMALS // 3 소수점 자릿수 +_SET_DATEFORMAT // 4 날짜 형식 +_SET_EPOCH // 5 년도 기준 +_SET_DELETED // 8 삭제 레코드 필터 +_SET_EXCLUSIVE // 11 독점 모드 +_SET_SOFTSEEK // 12 소프트 검색 +``` + +## ErrorBlock / Break — 에러 처리 + +Harbour 호환 구조적 에러 처리: + +### BEGIN SEQUENCE / RECOVER + +```prg +LOCAL bOldError +LOCAL oErr + +// 에러 핸들러 설정 +bOldError := ErrorBlock({|e| Break(e)}) + +BEGIN SEQUENCE + // 에러가 발생할 수 있는 코드 + USE "nonexistent.dbf" +RECOVER USING oErr + // oErr는 에러 객체 (Hash) + ? oErr["DESCRIPTION"] // 에러 설명 + ? oErr["OPERATION"] // 실패한 연산 + ? oErr["SUBSYSTEM"] // 서브시스템 이름 + ? oErr["GENCODE"] // 일반 에러 코드 +END SEQUENCE + +// 이전 핸들러 복원 +ErrorBlock(bOldError) +``` + +### ErrorBlock + +```prg +// 현재 에러 핸들러 가져오기 +LOCAL bHandler := ErrorBlock() + +// 새 핸들러 설정 (이전 핸들러 반환) +LOCAL bOld := ErrorBlock({|e| MyErrorHandler(e)}) + +FUNCTION MyErrorHandler(oErr) + ? "Error:", oErr["DESCRIPTION"] + ? "Operation:", oErr["OPERATION"] + BREAK oErr // BEGIN SEQUENCE의 RECOVER로 전달 +RETURN NIL +``` + +### ErrorNew + +```prg +LOCAL oErr := ErrorNew() +oErr["SUBSYSTEM"] := "MYAPP" +oErr["DESCRIPTION"] := "Custom error" +oErr["OPERATION"] := "MyFunc" +oErr["GENCODE"] := 1001 +oErr["SEVERITY"] := 2 // ES_ERROR +``` + +## MEMO 필드 — 투명한 읽기/쓰기 + +Five는 DBF의 MEMO 필드를 투명하게 처리합니다. +FPT 파일이 자동으로 생성/열림: + +```prg +// MEMO 필드가 있는 테이블 생성 — FPT 자동 생성 +USE "notes" NEW +APPEND BLANK +REPLACE NAME WITH "Charles" +REPLACE NOTES WITH "This is a long memo text..." // FPT에 자동 저장 +? NOTES // "This is a long memo text..." — FPT에서 자동 읽기 + +// 큰 메모도 문제없음 +REPLACE NOTES WITH REPLICATE("Large data. ", 1000) // ~12KB +? LEN(NOTES) // 12000 +``` + +### MEMO 내부 동작 + +| 동작 | 자동 처리 | +|------|-----------| +| DBF 생성 (M 필드 포함) | FPT 파일 자동 생성 | +| DBF 열기 (M 필드 포함) | FPT 파일 자동 열기 | +| REPLACE memo WITH text | FPT에 쓰기 → 블록 번호 DBF에 저장 | +| ? memo | 블록 번호로 FPT 읽기 → 문자열 반환 | +| DBF 닫기 | FPT 자동 닫기 | + ## 예제 파일 | 파일 | 설명 | diff --git a/hbrdd/dbf/dbf.go b/hbrdd/dbf/dbf.go index a74f6f2..30d08fc 100644 --- a/hbrdd/dbf/dbf.go +++ b/hbrdd/dbf/dbf.go @@ -17,6 +17,7 @@ import ( "five/hbrdd" "fmt" "os" + "strings" ) // DBFArea implements the DBF database driver. @@ -44,6 +45,9 @@ type DBFArea struct { recCount uint32 ghost bool // at phantom record (after APPEND) + // Memo file (FPT) + memoFile *FPTFile + // Index integration (NTX/CDX) idxState *indexState } @@ -92,6 +96,25 @@ func (d *dbfAliasDriver) Create(params hbrdd.CreateParams) (hbrdd.Area, error) { return createDBF(&DBFDriver{}, params) } +// fptPathFromDBF returns the FPT memo file path for a given DBF path. +func fptPathFromDBF(dbfPath string) string { + base := dbfPath + if idx := strings.LastIndex(base, "."); idx >= 0 { + base = base[:idx] + } + return base + ".fpt" +} + +// hasMemoField checks if any field descriptor is a MEMO type. +func hasMemoField(fields []FieldDesc) bool { + for _, f := range fields { + if f.Type == 'M' || f.Type == 'm' { + return true + } + } + return false +} + // --- Open --- // Harbour: hb_dbfOpen in dbf1.c func openDBF(drv *DBFDriver, params hbrdd.OpenParams) (*DBFArea, error) { @@ -178,7 +201,16 @@ func openDBF(drv *DBFDriver, params hbrdd.OpenParams) (*DBFArea, error) { } area.InitFields(fieldInfos) - // Step 8: Position at first record + // Step 8: Auto-open FPT if memo fields exist + if hasMemoField(fields) { + fptPath := fptPathFromDBF(path) + if fpt, err := OpenFPT(fptPath); err == nil { + area.memoFile = fpt + } + // If FPT doesn't exist, memo reads return empty string + } + + // Step 9: Position at first record area.FEof = (area.recCount == 0) if area.recCount > 0 { area.GoTo(1) @@ -212,9 +244,14 @@ func createDBF(drv *DBFDriver, params hbrdd.CreateParams) (*DBFArea, error) { } // Build header + hasMemo := hasMemoField(fieldDescs) headerLen := uint16(HeaderSize + len(fieldDescs)*FieldDescSize + 1) // +1 for terminator + version := byte(VersionDBF3) + if hasMemo { + version = VersionFPT + } hdr := Header{ - Version: VersionDBF3, + Version: version, RecCount: 0, HeaderLen: headerLen, RecordLen: recordLen, @@ -256,6 +293,17 @@ func createDBF(drv *DBFDriver, params hbrdd.CreateParams) (*DBFArea, error) { area.InitFields(fieldInfos) area.FEof = true + // Auto-create FPT if memo fields exist + if hasMemo { + fptPath := fptPathFromDBF(path) + fpt, err := CreateFPT(fptPath, FPTDefaultBlock) + if err != nil { + f.Close() + return nil, fmt.Errorf("create memo file: %w", err) + } + area.memoFile = fpt + } + return area, nil } @@ -268,11 +316,18 @@ func (a *DBFArea) Close() error { a.flushRecord() } a.updateHeader() + if a.memoFile != nil { + a.memoFile.Close() + a.memoFile = nil + } err := a.dataFile.Close() a.BaseArea.Close() return err } +// MemoFile returns the FPT memo file, or nil if no memo fields. +func (a *DBFArea) MemoFile() *FPTFile { return a.memoFile } + func (a *DBFArea) Flush() error { if a.dirty { if err := a.flushRecord(); err != nil { @@ -451,7 +506,21 @@ func (a *DBFArea) GetValue(fieldIndex int) (hbrt.Value, error) { if a.FEof { return hbrt.MakeNil(), nil } - return GetFieldValue(a.recBuf, a.offsets[fieldIndex], &a.fieldDescs[fieldIndex]), nil + fd := &a.fieldDescs[fieldIndex] + // MEMO field: read from FPT and return string + if (fd.Type == 'M' || fd.Type == 'm') && a.memoFile != nil { + blockVal := GetFieldValue(a.recBuf, a.offsets[fieldIndex], fd) + blockNo := uint32(blockVal.AsNumInt()) + if blockNo == 0 { + return hbrt.MakeString(""), nil + } + data, err := a.memoFile.ReadMemo(blockNo) + if err != nil { + return hbrt.MakeString(""), nil + } + return hbrt.MakeString(string(data)), nil + } + return GetFieldValue(a.recBuf, a.offsets[fieldIndex], fd), nil } func (a *DBFArea) PutValue(fieldIndex int, val hbrt.Value) error { @@ -461,7 +530,19 @@ func (a *DBFArea) PutValue(fieldIndex int, val hbrt.Value) error { if fieldIndex < 0 || fieldIndex >= len(a.fieldDescs) { return fmt.Errorf("field index out of range: %d", fieldIndex) } - PutFieldValue(a.recBuf, a.offsets[fieldIndex], &a.fieldDescs[fieldIndex], val) + fd := &a.fieldDescs[fieldIndex] + // MEMO field: write string to FPT, store block number in DBF + if (fd.Type == 'M' || fd.Type == 'm') && a.memoFile != nil && val.IsString() { + data := []byte(val.AsString()) + blockNo, err := a.memoFile.WriteMemo(data) + if err != nil { + return fmt.Errorf("write memo: %w", err) + } + putMemoRef(a.recBuf[a.offsets[fieldIndex]:a.offsets[fieldIndex]+uint16(fd.Len)], fd.Len, blockNo) + a.dirty = true + return nil + } + PutFieldValue(a.recBuf, a.offsets[fieldIndex], fd, val) a.dirty = true return nil } diff --git a/hbrdd/dbf/dbf_integration_test.go b/hbrdd/dbf/dbf_integration_test.go index 2244e80..7acb987 100644 --- a/hbrdd/dbf/dbf_integration_test.go +++ b/hbrdd/dbf/dbf_integration_test.go @@ -12,6 +12,7 @@ import ( "math" "os" "path/filepath" + "strings" "testing" ) @@ -199,77 +200,58 @@ func TestDBFCDX_Create100(t *testing.T) { } } -// TestDBF_MemoField tests memo field with FPT file. +// TestDBF_MemoField tests transparent memo field read/write with auto-managed FPT. func TestDBF_MemoField(t *testing.T) { dir := t.TempDir() dbfPath := filepath.Join(dir, "memo_test") fptPath := dbfPath + ".fpt" - // Create DBF with memo field + // Create DBF with memo field — FPT auto-created drv := &DBFDriver{} area, err := drv.Create(hbrdd.CreateParams{ Path: dbfPath, Fields: []hbrdd.FieldInfo{ {Name: "ID", Type: 'N', Len: 5}, {Name: "TITLE", Type: 'C', Len: 50}, - {Name: "NOTES", Type: 'M', Len: 10}, // Memo field + {Name: "NOTES", Type: 'M', Len: 10}, }, }) if err != nil { t.Fatal(err) } - // Create FPT file alongside DBF - fpt, err := CreateFPT(fptPath, 64) - if err != nil { - area.Close() - t.Fatal(err) - } - - // Insert 100 records with memo data + // Insert 100 records with memo data (transparent write) for i := 1; i <= 100; i++ { area.Append() area.PutValue(0, hbrt.MakeInt(i)) area.PutValue(1, hbrt.MakeString(fmt.Sprintf("Item %d", i))) - // Write memo to FPT and store block number in DBF memoText := fmt.Sprintf("This is memo #%d with some longer text for testing. Record number: %d. "+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt.", i, i) - blockNo, err := fpt.WriteMemo([]byte(memoText)) - if err != nil { - t.Fatalf("write memo %d: %v", i, err) - } - area.PutValue(2, hbrt.MakeLong(int64(blockNo))) + area.PutValue(2, hbrt.MakeString(memoText)) area.Flush() } area.Close() - fpt.Close() - // Reopen and verify + // Verify FPT file exists + if _, err := os.Stat(fptPath); err != nil { + t.Error("FPT memo file should exist") + } + + // Verify no DBT file + dbtPath := dbfPath + ".dbt" + if _, err := os.Stat(dbtPath); err == nil { + t.Error("DBT file should NOT exist for FPT") + } + + // Reopen and verify (transparent read) area2, err := drv.Open(hbrdd.OpenParams{Path: dbfPath}) if err != nil { t.Fatal(err) } defer area2.Close() - fpt2, err := OpenFPT(fptPath) - if err != nil { - t.Fatal(err) - } - defer fpt2.Close() - - // Verify FPT file exists (CDX uses FPT, not DBT) - if _, err := os.Stat(fptPath); err != nil { - t.Error("FPT memo file should exist") - } - - // Verify no DBT file (CDX doesn't use DBT) - dbtPath := dbfPath + ".dbt" - if _, err := os.Stat(dbtPath); err == nil { - t.Error("DBT file should NOT exist for CDX/FPT") - } - rc, _ := area2.RecCount() if rc != 100 { t.Fatalf("reccount = %d, want 100", rc) @@ -277,17 +259,8 @@ func TestDBF_MemoField(t *testing.T) { // Read and verify memo for record 1 area2.GoTo(1) - memoBlockVal, _ := area2.GetValue(2) - blockNo := uint32(memoBlockVal.AsNumInt()) - if blockNo == 0 { - t.Fatal("rec1 memo block should not be 0") - } - - memoData, err := fpt2.ReadMemo(blockNo) - if err != nil { - t.Fatal(err) - } - memoStr := string(memoData) + v, _ := area2.GetValue(2) + memoStr := v.AsString() if len(memoStr) < 10 { t.Errorf("rec1 memo too short: %d bytes", len(memoStr)) } @@ -297,37 +270,21 @@ func TestDBF_MemoField(t *testing.T) { // Read and verify memo for record 50 area2.GoTo(50) - memoBlockVal, _ = area2.GetValue(2) - blockNo = uint32(memoBlockVal.AsNumInt()) - memoData, err = fpt2.ReadMemo(blockNo) - if err != nil { - t.Fatal(err) - } - memoStr = string(memoData) + v, _ = area2.GetValue(2) + memoStr = v.AsString() if memoStr[:17] != "This is memo #50 " { t.Errorf("rec50 memo start = %q", memoStr[:20]) } // Read and verify memo for record 100 area2.GoTo(100) - memoBlockVal, _ = area2.GetValue(2) - blockNo = uint32(memoBlockVal.AsNumInt()) - memoData, err = fpt2.ReadMemo(blockNo) - if err != nil { - t.Fatal(err) - } - memoStr = string(memoData) + v, _ = area2.GetValue(2) + memoStr = v.AsString() if memoStr[:18] != "This is memo #100 " { t.Errorf("rec100 memo start = %q", memoStr[:20]) } - // Verify FPT block size - if fpt2.blockSize != 64 { - t.Errorf("FPT block size = %d, want 64", fpt2.blockSize) - } - - t.Logf("Memo test: 100 records with FPT memo verified") - t.Logf("FPT block size: %d, next block: %d", fpt2.blockSize, fpt2.header.NextBlock) + t.Logf("Memo test: 100 records with FPT memo verified (transparent API)") } // TestDBFCDX_MemoIsFPT confirms that DBFCDX uses FPT format, not DBT. @@ -419,7 +376,7 @@ func TestDBF_AllFieldTypes(t *testing.T) { area.PutValue(2, hbrt.MakeInt(999999)) area.PutValue(3, hbrt.MakeBool(true)) area.PutValue(4, hbrt.MakeDate(dateToJulian(2026, 3, 28))) - area.PutValue(5, hbrt.MakeLong(0)) // empty memo + area.PutValue(5, hbrt.MakeString("Memo for rec1")) // memo text area.Flush() area.Append() @@ -428,7 +385,7 @@ func TestDBF_AllFieldTypes(t *testing.T) { area.PutValue(2, hbrt.MakeInt(-1)) // negative area.PutValue(3, hbrt.MakeBool(false)) area.PutValue(4, hbrt.MakeDate(0)) // empty date - area.PutValue(5, hbrt.MakeLong(0)) + area.PutValue(5, hbrt.MakeString("")) // empty memo area.Flush() area.Close() @@ -485,3 +442,88 @@ func TestDBF_AllFieldTypes(t *testing.T) { t.Log("All field types test passed") } + +// TestDBF_TransparentMemo tests transparent MEMO read/write through PutValue/GetValue. +// User writes a string → DBF auto-writes to FPT → user reads back a string. +func TestDBF_TransparentMemo(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "memo_transparent") + + drv := &DBFDriver{} + area, err := drv.Create(hbrdd.CreateParams{ + Path: path, + Fields: []hbrdd.FieldInfo{ + {Name: "NAME", Type: 'C', Len: 20}, + {Name: "NOTES", Type: 'M', Len: 10}, + }, + }) + if err != nil { + t.Fatal(err) + } + + // Verify FPT was auto-created + dbfArea := area.(*DBFArea) + if dbfArea.MemoFile() == nil { + t.Fatal("FPT should be auto-created for MEMO fields") + } + + // Record 1: normal memo + area.Append() + area.PutValue(0, hbrt.MakeString("Alice")) + area.PutValue(1, hbrt.MakeString("This is Alice's memo with some longer text for testing.")) + area.Flush() + + // Record 2: empty memo + area.Append() + area.PutValue(0, hbrt.MakeString("Bob")) + area.PutValue(1, hbrt.MakeString("")) + area.Flush() + + // Record 3: large memo + largeMemo := strings.Repeat("Large memo data. ", 100) // ~1700 bytes + area.Append() + area.PutValue(0, hbrt.MakeString("Charlie")) + area.PutValue(1, hbrt.MakeString(largeMemo)) + area.Flush() + + area.Close() + + // Reopen and verify transparent read + area2, err := drv.Open(hbrdd.OpenParams{Path: path}) + if err != nil { + t.Fatal(err) + } + defer area2.Close() + + // Verify FPT auto-opened + if area2.(*DBFArea).MemoFile() == nil { + t.Fatal("FPT should be auto-opened on Open") + } + + // Record 1 + area2.GoTo(1) + v, _ := area2.GetValue(0) + if strings.TrimSpace(v.AsString()) != "Alice" { + t.Errorf("rec1 NAME = %q", v.AsString()) + } + v, _ = area2.GetValue(1) + if v.AsString() != "This is Alice's memo with some longer text for testing." { + t.Errorf("rec1 NOTES = %q", v.AsString()) + } + + // Record 2: empty memo + area2.GoTo(2) + v, _ = area2.GetValue(1) + if v.AsString() != "" { + t.Errorf("rec2 NOTES should be empty, got %q", v.AsString()) + } + + // Record 3: large memo + area2.GoTo(3) + v, _ = area2.GetValue(1) + if v.AsString() != largeMemo { + t.Errorf("rec3 NOTES length = %d, want %d", len(v.AsString()), len(largeMemo)) + } + + t.Logf("Transparent MEMO: 3 records verified (normal/empty/large)") +}