// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // Integration tests: create DBF with various field types, // insert 100 records, verify read-back, test memo fields. package dbf import ( "five/hbrt" "five/hbrdd" "fmt" "math" "os" "path/filepath" "strings" "testing" ) // TestDBFNTX_Create100 creates a DBFNTX-style DBF with 100 records. func TestDBFNTX_Create100(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "ntx_test") drv := &DBFDriver{} area, err := drv.Create(hbrdd.CreateParams{ Path: path, Fields: []hbrdd.FieldInfo{ {Name: "ID", Type: 'N', Len: 10, Dec: 0}, {Name: "NAME", Type: 'C', Len: 30}, {Name: "SALARY", Type: 'N', Len: 12, Dec: 2}, {Name: "ACTIVE", Type: 'L', Len: 1}, {Name: "HIREDATE", Type: 'D', Len: 8}, }, }) if err != nil { t.Fatal(err) } // Verify DBF version byte (NTX style = standard DBF3) dbfArea := area.(*DBFArea) if dbfArea.header.Version != VersionDBF3 { t.Errorf("DBFNTX version = 0x%02X, want 0x%02X", dbfArea.header.Version, VersionDBF3) } // Insert 100 records names := []string{"Kim", "Lee", "Park", "Choi", "Jung", "Kang", "Cho", "Yoon", "Jang", "Lim"} for i := 1; i <= 100; i++ { area.Append() area.PutValue(0, hbrt.MakeInt(i)) area.PutValue(1, hbrt.MakeString(fmt.Sprintf("%s_%03d", names[i%10], i))) area.PutValue(2, hbrt.MakeDouble(30000.0+float64(i)*500.50, 12, 2)) area.PutValue(3, hbrt.MakeBool(i%3 != 0)) area.PutValue(4, hbrt.MakeDate(dateToJulian(2020, (i%12)+1, (i%28)+1))) area.Flush() } area.Close() // Reopen and verify area2, err := drv.Open(hbrdd.OpenParams{Path: path}) if err != nil { t.Fatal(err) } defer area2.Close() rc, _ := area2.RecCount() if rc != 100 { t.Fatalf("reccount = %d, want 100", rc) } // Verify field count and types if area2.FieldCount() != 5 { t.Fatalf("fieldcount = %d, want 5", area2.FieldCount()) } // Verify record 1 area2.GoTo(1) id, _ := area2.GetValue(0) name, _ := area2.GetValue(1) salary, _ := area2.GetValue(2) active, _ := area2.GetValue(3) hdate, _ := area2.GetValue(4) if id.AsNumInt() != 1 { t.Errorf("rec1 ID = %d, want 1", id.AsNumInt()) } if name.AsString()[:7] != "Lee_001" { t.Errorf("rec1 NAME = %q", name.AsString()) } if math.Abs(salary.AsDouble()-30500.50) > 0.01 { t.Errorf("rec1 SALARY = %f, want 30500.50", salary.AsDouble()) } if !active.AsBool() { t.Error("rec1 ACTIVE should be .T.") } if !hdate.IsDate() { t.Error("rec1 HIREDATE should be date type") } // Verify record 50 area2.GoTo(50) id, _ = area2.GetValue(0) if id.AsNumInt() != 50 { t.Errorf("rec50 ID = %d, want 50", id.AsNumInt()) } // Verify record 100 area2.GoTo(100) id, _ = area2.GetValue(0) if id.AsNumInt() != 100 { t.Errorf("rec100 ID = %d, want 100", id.AsNumInt()) } // Verify sequential scan area2.GoTop() count := 0 for !area2.EOF() { count++ area2.Skip(1) } if count != 100 { t.Errorf("scan count = %d, want 100", count) } // Verify backward scan area2.GoBottom() count = 0 for !area2.BOF() { count++ area2.Skip(-1) } if count != 100 { t.Errorf("backward scan = %d, want 100", count) } // Verify file extension if _, err := os.Stat(path + ".dbf"); err != nil { t.Error("DBF file should exist") } } // TestDBFCDX_Create100 creates a DBFCDX-style DBF (FPT version byte). func TestDBFCDX_Create100(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "cdx_test") // Create with FPT version (CDX style) drv := &DBFDriver{} area, err := drv.Create(hbrdd.CreateParams{ Path: path, Fields: []hbrdd.FieldInfo{ {Name: "CODE", Type: 'C', Len: 10}, {Name: "AMOUNT", Type: 'N', Len: 15, Dec: 4}, {Name: "FLAG", Type: 'L', Len: 1}, {Name: "TXDATE", Type: 'D', Len: 8}, }, }) if err != nil { t.Fatal(err) } // Insert 100 records for i := 1; i <= 100; i++ { area.Append() area.PutValue(0, hbrt.MakeString(fmt.Sprintf("TX%06d", i))) area.PutValue(1, hbrt.MakeDouble(float64(i)*123.4567, 15, 4)) area.PutValue(2, hbrt.MakeBool(i%2 == 0)) area.PutValue(3, hbrt.MakeDate(dateToJulian(2025, (i%12)+1, (i%28)+1))) area.Flush() } area.Close() // Reopen and verify area2, err := drv.Open(hbrdd.OpenParams{Path: path}) if err != nil { t.Fatal(err) } defer area2.Close() rc, _ := area2.RecCount() if rc != 100 { t.Fatalf("reccount = %d, want 100", rc) } // Verify record 1 area2.GoTo(1) code, _ := area2.GetValue(0) if code.AsString()[:8] != "TX000001" { t.Errorf("rec1 CODE = %q", code.AsString()) } amount, _ := area2.GetValue(1) if math.Abs(amount.AsDouble()-123.4567) > 0.001 { t.Errorf("rec1 AMOUNT = %f", amount.AsDouble()) } // Verify record 100 area2.GoTo(100) code, _ = area2.GetValue(0) if code.AsString()[:8] != "TX000100" { t.Errorf("rec100 CODE = %q", code.AsString()) } } // 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 — 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}, }, }) if err != nil { t.Fatal(err) } // 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))) 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) area.PutValue(2, hbrt.MakeString(memoText)) area.Flush() } area.Close() // 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() rc, _ := area2.RecCount() if rc != 100 { t.Fatalf("reccount = %d, want 100", rc) } // Read and verify memo for record 1 area2.GoTo(1) v, _ := area2.GetValue(2) memoStr := v.AsString() if len(memoStr) < 10 { t.Errorf("rec1 memo too short: %d bytes", len(memoStr)) } if memoStr[:16] != "This is memo #1 " { t.Errorf("rec1 memo start = %q", memoStr[:20]) } // Read and verify memo for record 50 area2.GoTo(50) 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) v, _ = area2.GetValue(2) memoStr = v.AsString() if memoStr[:18] != "This is memo #100 " { t.Errorf("rec100 memo start = %q", memoStr[:20]) } t.Logf("Memo test: 100 records with FPT memo verified (transparent API)") } // TestDBFCDX_MemoIsFPT confirms that DBFCDX uses FPT format, not DBT. func TestDBFCDX_MemoIsFPT(t *testing.T) { dir := t.TempDir() dbfPath := filepath.Join(dir, "cdx_memo_test") fptPath := dbfPath + ".fpt" dbtPath := dbfPath + ".dbt" // Create FPT (what DBFCDX would use) fpt, err := CreateFPT(fptPath, 64) if err != nil { t.Fatal(err) } // Write some memo data block1, _ := fpt.WriteMemo([]byte("FPT memo data for DBFCDX")) block2, _ := fpt.WriteMemo([]byte("Second memo entry")) fpt.Close() // Verify FPT exists if _, err := os.Stat(fptPath); err != nil { t.Fatal("FPT file must exist for DBFCDX") } // Verify DBT does NOT exist if _, err := os.Stat(dbtPath); err == nil { t.Fatal("DBT file must NOT exist for DBFCDX (uses FPT)") } // Reopen and read back fpt2, err := OpenFPT(fptPath) if err != nil { t.Fatal(err) } defer fpt2.Close() data1, _ := fpt2.ReadMemo(block1) if string(data1) != "FPT memo data for DBFCDX" { t.Errorf("memo 1 = %q", string(data1)) } data2, _ := fpt2.ReadMemo(block2) if string(data2) != "Second memo entry" { t.Errorf("memo 2 = %q", string(data2)) } // Verify Big-Endian header (FPT characteristic) f, _ := os.Open(fptPath) hdr := make([]byte, 8) f.ReadAt(hdr, 0) f.Close() // FPT header: nextBlock(4 BE), reserved(2), blockSize(2 BE) // Block size should be 64 in Big-Endian blockSizeBE := uint16(hdr[6])<<8 | uint16(hdr[7]) if blockSizeBE != 64 { t.Errorf("FPT block size (BE) = %d, want 64", blockSizeBE) } t.Logf("DBFCDX memo format: FPT confirmed (Big-Endian header, block size %d)", blockSizeBE) t.Logf("Block 1 at %d, Block 2 at %d", block1, block2) } // TestDBF_AllFieldTypes tests all common DBF field types. func TestDBF_AllFieldTypes(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "alltypes") drv := &DBFDriver{} area, err := drv.Create(hbrdd.CreateParams{ Path: path, Fields: []hbrdd.FieldInfo{ {Name: "CHAR", Type: 'C', Len: 20}, {Name: "NUM", Type: 'N', Len: 12, Dec: 2}, {Name: "INT", Type: 'N', Len: 10, Dec: 0}, {Name: "LOGIC", Type: 'L', Len: 1}, {Name: "DATE", Type: 'D', Len: 8}, {Name: "MEMO", Type: 'M', Len: 10}, }, }) if err != nil { t.Fatal(err) } // Insert test records area.Append() area.PutValue(0, hbrt.MakeString("Hello World")) area.PutValue(1, hbrt.MakeDouble(-12345.67, 12, 2)) 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.MakeString("Memo for rec1")) // memo text area.Flush() area.Append() area.PutValue(0, hbrt.MakeString("")) // empty string area.PutValue(1, hbrt.MakeDouble(0.0, 12, 2)) // zero 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.MakeString("")) // empty memo area.Flush() area.Close() // Reopen and verify area2, err := drv.Open(hbrdd.OpenParams{Path: path}) if err != nil { t.Fatal(err) } defer area2.Close() // Record 1 area2.GoTo(1) v, _ := area2.GetValue(0) if v.AsString()[:11] != "Hello World" { t.Errorf("CHAR = %q", v.AsString()) } v, _ = area2.GetValue(1) if math.Abs(v.AsDouble()-(-12345.67)) > 0.01 { t.Errorf("NUM = %f, want -12345.67", v.AsDouble()) } v, _ = area2.GetValue(2) if v.AsNumInt() != 999999 { t.Errorf("INT = %d, want 999999", v.AsNumInt()) } v, _ = area2.GetValue(3) if !v.AsBool() { t.Error("LOGIC should be TRUE") } v, _ = area2.GetValue(4) y, m, d := julianToDate(v.AsJulian()) if y != 2026 || m != 3 || d != 28 { t.Errorf("DATE = %d-%d-%d, want 2026-3-28", y, m, d) } // Record 2 — edge cases area2.GoTo(2) v, _ = area2.GetValue(1) if v.AsDouble() != 0.0 { t.Errorf("zero NUM = %f", v.AsDouble()) } v, _ = area2.GetValue(2) if v.AsNumInt() != -1 { t.Errorf("negative INT = %d", v.AsNumInt()) } v, _ = area2.GetValue(3) if v.AsBool() { t.Error("LOGIC should be FALSE") } v, _ = area2.GetValue(4) if v.AsJulian() != 0 { t.Error("empty date should be julian 0") } 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)") }