feat: transparent MEMO read/write + documentation update
DBFArea auto-manages FPT memo files: - Create/Open: auto-creates/opens FPT when memo fields exist - PutValue: string on MEMO field auto-writes to FPT - GetValue: MEMO field auto-reads from FPT, returns string - Close: auto-closes FPT Documentation: Value methods, MEMVAR, SET, ErrorBlock, MEMO added to five-syntax-ko.md and five-syntax-en.md (+480 lines) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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 자동 닫기 |
|
||||
|
||||
## 예제 파일
|
||||
|
||||
| 파일 | 설명 |
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user