Bug 1: FIELD->NAME in INDEX ON expression - evalKeyExprInner: strip FIELD->/alias-> prefix before field lookup - exprToString: handle AliasExpr (FIELD->NAME → "FIELD->NAME") Bug 2: AsNumInt() on Double returned IEEE 754 raw bits - Value.AsNumInt(): check tDouble and convert via Float64frombits - Fixed array index crash when index is result of % modulo Bug 3: PACK/ZAP crash with open indexes - OrderListRebuild: fully implemented (was TODO stub) Saves index info, closes all, sets idxState=nil, recreates - OrderCreate: set current=-1 during key evaluation (natural GoTo) - PACK/ZAP: save/restore idxState, rebuild after operation - Register __DBPACK, __DBZAP, DBRECALL symbol aliases Harbour vs Five: 45/47 match (96%), 2 diffs are duplicate-key sort order Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
804 lines
17 KiB
Go
804 lines
17 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
// DBFArea — the core DBF file driver.
|
|
// Byte-compatible with Harbour/Clipper DBF files.
|
|
//
|
|
// Harbour equivalent: DBFAREA in dbf1.c
|
|
// Inherits from BaseArea (WAAREA), implements Area + RecordManager + Locker.
|
|
//
|
|
// Reference:
|
|
// /mnt/d/harbour-core/src/rdd/dbf1.c
|
|
// docs/dbf-engine-spec.md
|
|
package dbf
|
|
|
|
import (
|
|
"five/hbrt"
|
|
"five/hbrdd"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// DBFArea implements the DBF database driver.
|
|
// Harbour: struct DBFAREA in dbf1.c
|
|
type DBFArea struct {
|
|
hbrdd.BaseArea // embed WAAREA defaults
|
|
|
|
// File handles
|
|
dataFile *os.File
|
|
filePath string
|
|
shared bool
|
|
readOnly bool
|
|
|
|
// Header
|
|
header Header
|
|
fieldDescs []FieldDesc
|
|
offsets []uint16 // field byte offsets within record
|
|
|
|
// Record buffer
|
|
recBuf []byte // current record (RecordLen bytes)
|
|
recNo uint32 // current record number (1-based)
|
|
dirty bool // record buffer modified
|
|
|
|
// State
|
|
recCount uint32
|
|
ghost bool // at phantom record (after APPEND)
|
|
|
|
// Memo file (FPT)
|
|
memoFile *FPTFile
|
|
|
|
// Index integration (NTX/CDX)
|
|
idxState *indexState
|
|
}
|
|
|
|
// DBFDriver is the driver factory for DBF files.
|
|
type DBFDriver struct{}
|
|
|
|
func (d *DBFDriver) Name() string { return "DBF" }
|
|
|
|
func (d *DBFDriver) Open(params hbrdd.OpenParams) (hbrdd.Area, error) {
|
|
return openDBF(d, params)
|
|
}
|
|
|
|
func (d *DBFDriver) Create(params hbrdd.CreateParams) (hbrdd.Area, error) {
|
|
return createDBF(d, params)
|
|
}
|
|
|
|
func init() {
|
|
hbrdd.RegisterDriver(&DBFDriver{})
|
|
// Register aliases used by Harbour
|
|
hbrdd.RegisterDriver(&dbfAliasDriver{name: "DBFNTX"})
|
|
hbrdd.RegisterDriver(&dbfAliasDriver{name: "DBFCDX"})
|
|
hbrdd.RegisterDriver(&dbfAliasDriver{name: "DBFFPT"})
|
|
// SIX compatible drivers — same DBF engine, different semantics handled at higher level
|
|
hbrdd.RegisterDriver(&dbfAliasDriver{name: "SIXCDX"})
|
|
hbrdd.RegisterDriver(&dbfAliasDriver{name: "DBFNSX"})
|
|
hbrdd.RegisterDriver(&dbfAliasDriver{name: "DBFSIX"})
|
|
// Transfer format aliases
|
|
hbrdd.RegisterDriver(&dbfAliasDriver{name: "DBFDBT"})
|
|
// Bitmap/Rushmore RDD variants
|
|
hbrdd.RegisterDriver(&dbfAliasDriver{name: "BMDBFNTX"})
|
|
hbrdd.RegisterDriver(&dbfAliasDriver{name: "BMDBFCDX"})
|
|
hbrdd.RegisterDriver(&dbfAliasDriver{name: "BMDBFNSX"})
|
|
}
|
|
|
|
// dbfAliasDriver wraps DBFDriver with a different name.
|
|
type dbfAliasDriver struct {
|
|
name string
|
|
}
|
|
|
|
func (d *dbfAliasDriver) Name() string { return d.name }
|
|
func (d *dbfAliasDriver) Open(params hbrdd.OpenParams) (hbrdd.Area, error) {
|
|
return openDBF(&DBFDriver{}, params)
|
|
}
|
|
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) {
|
|
path := params.Path
|
|
if !hasExtension(path) {
|
|
path += ".dbf"
|
|
}
|
|
|
|
flag := os.O_RDWR
|
|
if params.ReadOnly {
|
|
flag = os.O_RDONLY
|
|
}
|
|
|
|
f, err := os.OpenFile(path, flag, 0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open %s: %w", path, err)
|
|
}
|
|
|
|
area := &DBFArea{
|
|
dataFile: f,
|
|
filePath: path,
|
|
shared: params.Shared,
|
|
readOnly: params.ReadOnly,
|
|
}
|
|
area.BaseArea = hbrdd.BaseArea{}
|
|
|
|
// Step 1: Read header (32 bytes)
|
|
hdr, err := ReadHeader(f)
|
|
if err != nil {
|
|
f.Close()
|
|
return nil, err
|
|
}
|
|
area.header = *hdr
|
|
|
|
// Step 2: Read field descriptors
|
|
fieldCount := hdr.FieldCount()
|
|
if fieldCount <= 0 {
|
|
f.Close()
|
|
return nil, fmt.Errorf("invalid field count: %d", fieldCount)
|
|
}
|
|
|
|
fields, err := ReadFieldDescs(f, fieldCount)
|
|
if err != nil {
|
|
f.Close()
|
|
return nil, err
|
|
}
|
|
area.fieldDescs = fields
|
|
|
|
// Step 3: Build field offsets
|
|
area.offsets = BuildFieldOffsets(fields)
|
|
|
|
// Step 4: Validate record length
|
|
expectedRecLen := area.offsets[len(fields)]
|
|
if uint16(expectedRecLen) != hdr.RecordLen {
|
|
// Allow minor discrepancy (some DBF writers add extra bytes)
|
|
if uint16(expectedRecLen) > hdr.RecordLen {
|
|
f.Close()
|
|
return nil, fmt.Errorf("field offsets (%d) exceed record length (%d)",
|
|
expectedRecLen, hdr.RecordLen)
|
|
}
|
|
}
|
|
|
|
// Step 5: Allocate record buffer
|
|
area.recBuf = make([]byte, hdr.RecordLen)
|
|
|
|
// Step 6: Set record count (shared mode: recalculate from file size)
|
|
if params.Shared {
|
|
fileSize, _ := f.Seek(0, 2)
|
|
area.recCount = uint32((fileSize - int64(hdr.HeaderLen)) / int64(hdr.RecordLen))
|
|
} else {
|
|
area.recCount = hdr.RecCount
|
|
}
|
|
|
|
// Step 7: Build FieldInfo for BaseArea
|
|
fieldInfos := make([]hbrdd.FieldInfo, fieldCount)
|
|
for i, fd := range fields {
|
|
fieldInfos[i] = hbrdd.FieldInfo{
|
|
Name: fd.GetName(),
|
|
Type: fd.Type,
|
|
Len: int(fd.Len),
|
|
Dec: int(fd.Dec),
|
|
Flags: fd.Flags,
|
|
}
|
|
}
|
|
area.InitFields(fieldInfos)
|
|
|
|
// 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)
|
|
}
|
|
|
|
return area, nil
|
|
}
|
|
|
|
// --- Create ---
|
|
func createDBF(drv *DBFDriver, params hbrdd.CreateParams) (*DBFArea, error) {
|
|
path := params.Path
|
|
if !hasExtension(path) {
|
|
path += ".dbf"
|
|
}
|
|
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create %s: %w", path, err)
|
|
}
|
|
|
|
// Build field descriptors
|
|
fieldDescs := make([]FieldDesc, len(params.Fields))
|
|
recordLen := uint16(1) // deletion flag
|
|
for i, fi := range params.Fields {
|
|
fieldDescs[i].SetName(fi.Name)
|
|
fieldDescs[i].Type = fi.Type
|
|
fieldDescs[i].Len = byte(fi.Len)
|
|
fieldDescs[i].Dec = byte(fi.Dec)
|
|
fieldDescs[i].Flags = fi.Flags
|
|
recordLen += uint16(fi.Len)
|
|
}
|
|
|
|
// 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: version,
|
|
RecCount: 0,
|
|
HeaderLen: headerLen,
|
|
RecordLen: recordLen,
|
|
}
|
|
hdr.UpdateDate()
|
|
|
|
// Write header
|
|
if err := WriteHeader(f, &hdr); err != nil {
|
|
f.Close()
|
|
return nil, err
|
|
}
|
|
|
|
// Write field descriptors
|
|
if err := WriteFieldDescs(f, fieldDescs); err != nil {
|
|
f.Close()
|
|
return nil, err
|
|
}
|
|
|
|
// Write header terminator
|
|
f.Write([]byte{HeaderTerminator})
|
|
|
|
// Write EOF marker
|
|
f.Write([]byte{EOFMarker})
|
|
|
|
f.Seek(0, 0)
|
|
|
|
area := &DBFArea{
|
|
dataFile: f,
|
|
filePath: path,
|
|
header: hdr,
|
|
fieldDescs: fieldDescs,
|
|
offsets: BuildFieldOffsets(fieldDescs),
|
|
recBuf: make([]byte, recordLen),
|
|
recCount: 0,
|
|
}
|
|
|
|
fieldInfos := make([]hbrdd.FieldInfo, len(params.Fields))
|
|
copy(fieldInfos, params.Fields)
|
|
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
|
|
}
|
|
|
|
// --- Area interface ---
|
|
|
|
func (a *DBFArea) Driver() hbrdd.Driver { return &DBFDriver{} }
|
|
|
|
func (a *DBFArea) Close() error {
|
|
if a.dirty {
|
|
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 {
|
|
return err
|
|
}
|
|
}
|
|
return a.dataFile.Sync()
|
|
}
|
|
|
|
// --- Movement ---
|
|
|
|
func (a *DBFArea) RecNo() uint32 { return a.recNo }
|
|
|
|
func (a *DBFArea) RecCount() (uint32, error) {
|
|
if a.shared {
|
|
// Recalculate from file size (Harbour behavior)
|
|
size, err := a.dataFile.Seek(0, 2)
|
|
if err != nil {
|
|
return a.recCount, err
|
|
}
|
|
a.recCount = uint32((size - int64(a.header.HeaderLen)) / int64(a.header.RecordLen))
|
|
}
|
|
return a.recCount, nil
|
|
}
|
|
|
|
func (a *DBFArea) Deleted() bool {
|
|
if len(a.recBuf) > 0 {
|
|
return a.recBuf[0] == RecordDeleted
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GoTo positions the cursor at a specific record number.
|
|
// Harbour: hb_dbfGoTo in dbf1.c
|
|
func (a *DBFArea) GoTo(recNo uint32) error {
|
|
if a.dirty {
|
|
a.flushRecord()
|
|
}
|
|
|
|
a.FFound = false
|
|
|
|
if recNo == 0 || recNo > a.recCount {
|
|
// EOF / phantom record
|
|
a.recNo = a.recCount + 1
|
|
a.FEof = true
|
|
a.FBof = (recNo == 0)
|
|
// Clear buffer
|
|
for i := range a.recBuf {
|
|
a.recBuf[i] = ' '
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Read record from file
|
|
offset := a.header.RecordOffset(recNo)
|
|
_, err := a.dataFile.ReadAt(a.recBuf, offset)
|
|
if err != nil {
|
|
return fmt.Errorf("read record %d: %w", recNo, err)
|
|
}
|
|
|
|
a.recNo = recNo
|
|
a.FEof = false
|
|
a.FBof = false
|
|
a.dirty = false
|
|
a.ghost = false
|
|
return nil
|
|
}
|
|
|
|
// GoTop positions at the first record.
|
|
// Harbour: hb_dbfGoTop
|
|
func (a *DBFArea) GoTop() error {
|
|
// Use index order if active
|
|
if a.idxState != nil && a.idxState.current >= 0 {
|
|
a.FTop = true
|
|
a.FBottom = false
|
|
return a.GoTopIndexed()
|
|
}
|
|
|
|
a.FTop = true
|
|
a.FBottom = false
|
|
|
|
if a.recCount == 0 {
|
|
a.FEof = true
|
|
a.recNo = 1
|
|
return nil
|
|
}
|
|
|
|
if err := a.GoTo(1); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Skip filtered/deleted records
|
|
return a.skipFilter(1)
|
|
}
|
|
|
|
// GoBottom positions at the last record.
|
|
// Harbour: hb_dbfGoBottom
|
|
func (a *DBFArea) GoBottom() error {
|
|
// Use index order if active
|
|
if a.idxState != nil && a.idxState.current >= 0 {
|
|
a.FTop = false
|
|
a.FBottom = true
|
|
return a.GoBottomIndexed()
|
|
}
|
|
|
|
a.FTop = false
|
|
a.FBottom = true
|
|
|
|
if a.recCount == 0 {
|
|
a.FEof = true
|
|
a.recNo = 1
|
|
return nil
|
|
}
|
|
|
|
if err := a.GoTo(a.recCount); err != nil {
|
|
return err
|
|
}
|
|
|
|
return a.skipFilter(-1)
|
|
}
|
|
|
|
// Skip moves the cursor by count records.
|
|
// Harbour: hb_dbfSkip
|
|
func (a *DBFArea) Skip(count int64) error {
|
|
// Use index order if active
|
|
if a.idxState != nil && a.idxState.current >= 0 {
|
|
a.FTop = false
|
|
a.FBottom = false
|
|
return a.SkipIndexed(count)
|
|
}
|
|
|
|
a.FTop = false
|
|
a.FBottom = false
|
|
|
|
if count == 0 {
|
|
// Skip 0 = re-evaluate filter at current position
|
|
return a.skipFilter(1)
|
|
}
|
|
|
|
if count > 0 {
|
|
for i := int64(0); i < count; i++ {
|
|
if a.FEof {
|
|
break
|
|
}
|
|
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
|
|
}
|
|
// Skip deleted records when SET DELETED ON
|
|
if err := a.skipFilter(1); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else {
|
|
for i := int64(0); i > count; i-- {
|
|
if a.recNo <= 1 {
|
|
a.FBof = true
|
|
break
|
|
}
|
|
if err := a.GoTo(a.recNo - 1); err != nil {
|
|
return err
|
|
}
|
|
if err := a.skipFilter(-1); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// --- Field access ---
|
|
|
|
func (a *DBFArea) GetValue(fieldIndex int) (hbrt.Value, error) {
|
|
if fieldIndex < 0 || fieldIndex >= len(a.fieldDescs) {
|
|
return hbrt.MakeNil(), fmt.Errorf("field index out of range: %d", fieldIndex)
|
|
}
|
|
if a.FEof {
|
|
return hbrt.MakeNil(), 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 {
|
|
if a.readOnly {
|
|
return fmt.Errorf("table is read-only")
|
|
}
|
|
if fieldIndex < 0 || fieldIndex >= len(a.fieldDescs) {
|
|
return fmt.Errorf("field index out of range: %d", fieldIndex)
|
|
}
|
|
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
|
|
}
|
|
|
|
// --- Record operations ---
|
|
|
|
// Append adds a new blank record.
|
|
// Harbour: hb_dbfAppend
|
|
func (a *DBFArea) Append() error {
|
|
if a.readOnly {
|
|
return fmt.Errorf("table is read-only")
|
|
}
|
|
|
|
if a.dirty {
|
|
a.flushRecord()
|
|
}
|
|
|
|
a.recCount++
|
|
a.recNo = a.recCount
|
|
a.header.RecCount = a.recCount
|
|
|
|
// Clear record buffer (all spaces)
|
|
for i := range a.recBuf {
|
|
a.recBuf[i] = ' '
|
|
}
|
|
|
|
// Write blank record
|
|
offset := a.header.RecordOffset(a.recNo)
|
|
if _, err := a.dataFile.WriteAt(a.recBuf, offset); err != nil {
|
|
a.recCount--
|
|
return fmt.Errorf("append record: %w", err)
|
|
}
|
|
|
|
// Write EOF marker
|
|
eofOffset := a.header.EOFOffset()
|
|
a.dataFile.WriteAt([]byte{EOFMarker}, eofOffset)
|
|
|
|
// Update header
|
|
a.updateHeader()
|
|
|
|
a.FEof = false
|
|
a.FBof = false
|
|
a.dirty = false
|
|
a.ghost = true
|
|
return nil
|
|
}
|
|
|
|
// Delete marks the current record as deleted.
|
|
func (a *DBFArea) Delete() error {
|
|
if a.readOnly || a.FEof {
|
|
return nil
|
|
}
|
|
a.recBuf[0] = RecordDeleted
|
|
a.dirty = true
|
|
return nil
|
|
}
|
|
|
|
// Recall undeletes the current record.
|
|
func (a *DBFArea) Recall() error {
|
|
if a.readOnly || a.FEof {
|
|
return nil
|
|
}
|
|
a.recBuf[0] = RecordActive
|
|
a.dirty = true
|
|
return nil
|
|
}
|
|
|
|
// Pack removes all deleted records.
|
|
// Harbour: hb_dbfPack — requires exclusive access.
|
|
func (a *DBFArea) Pack() error {
|
|
if a.readOnly {
|
|
return fmt.Errorf("table is read-only")
|
|
}
|
|
if a.shared {
|
|
return fmt.Errorf("PACK requires exclusive access")
|
|
}
|
|
|
|
if a.dirty {
|
|
a.flushRecord()
|
|
}
|
|
|
|
// Temporarily disable index to avoid indexed navigation during PACK
|
|
var savedIdx *indexState
|
|
if a.idxState != nil {
|
|
savedIdx = a.idxState
|
|
a.idxState = nil
|
|
}
|
|
|
|
outRec := uint32(0)
|
|
buf := make([]byte, a.header.RecordLen)
|
|
|
|
for recNo := uint32(1); recNo <= a.recCount; recNo++ {
|
|
offset := a.header.RecordOffset(recNo)
|
|
if _, err := a.dataFile.ReadAt(buf, offset); err != nil {
|
|
return err
|
|
}
|
|
if buf[0] != RecordDeleted {
|
|
outRec++
|
|
outOffset := a.header.RecordOffset(outRec)
|
|
if _, err := a.dataFile.WriteAt(buf, outOffset); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
a.recCount = outRec
|
|
a.header.RecCount = outRec
|
|
|
|
// Truncate file
|
|
newSize := a.header.EOFOffset() + 1 // +1 for EOF marker
|
|
a.dataFile.Truncate(newSize)
|
|
|
|
// Write EOF
|
|
a.dataFile.WriteAt([]byte{EOFMarker}, a.header.EOFOffset())
|
|
|
|
// Update header
|
|
a.updateHeader()
|
|
|
|
// Reposition (natural order, no index yet)
|
|
if a.recCount > 0 {
|
|
a.GoTo(1)
|
|
} else {
|
|
a.FEof = true
|
|
a.recNo = 1
|
|
}
|
|
|
|
// Rebuild indexes (record numbers changed after PACK)
|
|
if savedIdx != nil {
|
|
a.idxState = savedIdx
|
|
if err := a.OrderListRebuild(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Zap removes all records.
|
|
func (a *DBFArea) Zap() error {
|
|
if a.readOnly || a.shared {
|
|
return fmt.Errorf("ZAP requires exclusive access")
|
|
}
|
|
|
|
// Save index state
|
|
var savedIdx *indexState
|
|
if a.idxState != nil {
|
|
savedIdx = a.idxState
|
|
a.idxState = nil
|
|
}
|
|
|
|
a.recCount = 0
|
|
a.header.RecCount = 0
|
|
|
|
// Truncate to header + EOF
|
|
a.dataFile.Truncate(int64(a.header.HeaderLen) + 1)
|
|
a.dataFile.WriteAt([]byte{EOFMarker}, int64(a.header.HeaderLen))
|
|
|
|
a.updateHeader()
|
|
a.FEof = true
|
|
a.recNo = 1
|
|
|
|
// Rebuild indexes (empty after ZAP)
|
|
if savedIdx != nil {
|
|
a.idxState = savedIdx
|
|
a.OrderListRebuild() // rebuilds empty indexes
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// --- Internal ---
|
|
|
|
func (a *DBFArea) flushRecord() error {
|
|
if !a.dirty || a.FEof {
|
|
return nil
|
|
}
|
|
offset := a.header.RecordOffset(a.recNo)
|
|
_, err := a.dataFile.WriteAt(a.recBuf, offset)
|
|
if err == nil {
|
|
a.dirty = false
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (a *DBFArea) updateHeader() {
|
|
a.header.RecCount = a.recCount
|
|
a.header.UpdateDate()
|
|
|
|
a.dataFile.Seek(0, 0)
|
|
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 {
|
|
if direction == 0 {
|
|
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
|
|
}
|
|
|
|
// --- Helpers ---
|
|
|
|
func hasExtension(path string) bool {
|
|
for i := len(path) - 1; i >= 0; i-- {
|
|
if path[i] == '.' {
|
|
return true
|
|
}
|
|
if path[i] == '/' || path[i] == '\\' {
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|