Cumulative season's silent-bug hunting (~62 fixes) across the FiveSql2 SQL engine, the Five compiler/runtime, and the hbrdd RDD layer. Saved as a single checkpoint before refactoring the parser to delegate xBase command translation to the preprocessor. Highlights: FiveSql2 engine (_FiveSql2/src/) - prefix-glob index attach -> explicit convention (<table>_pk.ntx, <table>_uq.ntx, <table>.cdx) — fixes silent multi-row INSERT row-drop - DROP/CREATE TABLE FErase chain extended (.cdx, .fsc, .fsv, .dbt, .fpt) - COUNT(DISTINCT col) parsed + aggregated via hSeen hash - UNION column-count mismatch returns SQL_ERR_GRAMMAR (was silent) - DISTINCT + ORDER BY hidden-col leak fixed (trim before DISTINCT) - Derived table FROM (SELECT...) + JOIN right-side derived - Self-FK CASCADE depth 2+ via SqlGetSingleColPK pre-collect - LAG/LEAD default arg uses SqlEvalRowExpr (handles -N const exprs) - DATE literal round-trip validation (Feb 29 non-leap rejected) - CREATE OR REPLACE VIEW; CREATE VIEW errors on already-exists - AlterTable type dispatcher comma-wrapped (1-char type "A" no longer matches CHARACTER) Compiler / runtime - gengo: HB_ -> FV_ prefix on emitted Go function names (Five identity) - gengo split: emit_block.go, emit_stmt.go, folding.go extracted - parser/stmtreg.go nudges - hbrt: debug TUI/CLI restructure (debugcmd, debugkey, termios_*), windows debug stubs collapsed - thread/vm/value/class/pcinterp tightening from panic traces RDD layer (hbrdd/) - dbf: null bitmap support (null.go + null_test.go), mmap split (mmap_posix.go / mmap_windows.go), byte-level numeric parse - ntx/cdx: windows mmap parity - workarea + mem RDD: cross-area state-bleed fixes RTL (hbrtl/) - errorlog rewrite with platform-specific FD (errorlog_fd_unix / errorlog_fd_other) - sqlscan, sqlhelpers, indexrtl, datetime extensions Gates green at checkpoint: - go test ./... : PASS - FiveSql2 SQL:1999 : 43/43 - Harbour compat : 56/56 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
197 lines
5.7 KiB
Go
197 lines
5.7 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
// DBF file header and field descriptor structures.
|
|
// EXACT byte-compatible with Harbour's DBFHEADER and DBFFIELD.
|
|
//
|
|
// Reference: /mnt/d/harbour-core/include/hbdbf.h
|
|
// docs/dbf-engine-spec.md
|
|
package dbf
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
)
|
|
|
|
// HeaderSize is the fixed size of the DBF file header.
|
|
const HeaderSize = 32
|
|
|
|
// FieldDescSize is the fixed size of each field descriptor.
|
|
const FieldDescSize = 32
|
|
|
|
// Header terminator byte.
|
|
const HeaderTerminator = 0x0D
|
|
|
|
// EOF marker byte.
|
|
const EOFMarker = 0x1A
|
|
|
|
// Deletion flag values.
|
|
const (
|
|
RecordActive = ' ' // 0x20
|
|
RecordDeleted = '*' // 0x2A
|
|
)
|
|
|
|
// Version byte values.
|
|
const (
|
|
VersionDBF3 = 0x03 // Standard dBASE III
|
|
VersionVFP = 0x30 // Visual FoxPro
|
|
VersionVFPAuto = 0x31 // VFP + autoincrement
|
|
VersionVFPVar = 0x32 // VFP + varchar/varbinary
|
|
VersionDBT = 0x83 // dBASE III + DBT memo
|
|
VersionFPT = 0xF5 // DBF + FPT memo
|
|
)
|
|
|
|
// Field flag bits (byte at field descriptor offset 18).
|
|
// Harbour: HB_FF_* in hbapirdd.h — matches our FieldInfo.Flags convention.
|
|
const (
|
|
FieldFlagSystem = 0x01 // system/hidden (not exposed as user-visible)
|
|
FieldFlagNullable = 0x02 // accepts SQL NULL, tracked via _NullFlags bit
|
|
FieldFlagBinary = 0x04 // binary payload (no codepage conversion)
|
|
FieldFlagAutoInc = 0x08 // auto-increment (VFP)
|
|
)
|
|
|
|
// NullFlagsFieldName is the hidden column Harbour/VFP uses to track
|
|
// SQL NULL state: 1 bit per nullable user column. Kept in fieldDescs
|
|
// but excluded from the public FieldCount/FieldInfo view so SQL
|
|
// `SELECT *` / DDL column enumeration never see it.
|
|
const NullFlagsFieldName = "_NullFlags"
|
|
|
|
// Header represents the 32-byte DBF file header.
|
|
// Layout is byte-identical to Harbour's DBFHEADER.
|
|
type Header struct {
|
|
Version byte // offset 0
|
|
Year byte // offset 1 (year - 1900)
|
|
Month byte // offset 2
|
|
Day byte // offset 3
|
|
RecCount uint32 // offset 4 (LE)
|
|
HeaderLen uint16 // offset 8 (LE)
|
|
RecordLen uint16 // offset 10 (LE)
|
|
Reserved1 [2]byte
|
|
Transaction byte // offset 14
|
|
Encrypted byte // offset 15
|
|
Reserved2 [12]byte
|
|
HasTags byte // offset 28
|
|
CodePage byte // offset 29
|
|
Reserved3 [2]byte
|
|
}
|
|
|
|
// ReadHeader reads the 32-byte header from a reader.
|
|
func ReadHeader(r io.Reader) (*Header, error) {
|
|
h := &Header{}
|
|
if err := binary.Read(r, binary.LittleEndian, h); err != nil {
|
|
return nil, fmt.Errorf("read DBF header: %w", err)
|
|
}
|
|
return h, nil
|
|
}
|
|
|
|
// WriteHeader writes the 32-byte header to a writer.
|
|
func WriteHeader(w io.Writer, h *Header) error {
|
|
return binary.Write(w, binary.LittleEndian, h)
|
|
}
|
|
|
|
// UpdateDate sets the header's last-update date to now.
|
|
func (h *Header) UpdateDate() {
|
|
now := time.Now()
|
|
h.Year = byte(now.Year() - 1900)
|
|
h.Month = byte(now.Month())
|
|
h.Day = byte(now.Day())
|
|
}
|
|
|
|
// FieldCount calculates the number of fields from header length.
|
|
// Harbour: fieldCount = (headerLen - 32 - 1) / 32
|
|
func (h *Header) FieldCount() int {
|
|
if h.HeaderLen < 33 {
|
|
return 0
|
|
}
|
|
return int(h.HeaderLen-32-1) / FieldDescSize
|
|
}
|
|
|
|
// HasMemo returns true if the version byte indicates a memo file.
|
|
func (h *Header) HasMemo() bool {
|
|
return h.Version == VersionDBT || h.Version == VersionFPT ||
|
|
h.Version == 0x8B || h.Version == 0xE6 || h.Version == 0xF6
|
|
}
|
|
|
|
// RecordOffset calculates the file offset for a record (1-based).
|
|
// Harbour: headerLen + (recNo - 1) * recordLen
|
|
func (h *Header) RecordOffset(recNo uint32) int64 {
|
|
return int64(h.HeaderLen) + int64(recNo-1)*int64(h.RecordLen)
|
|
}
|
|
|
|
// EOFOffset returns the file offset where EOF marker should be written.
|
|
func (h *Header) EOFOffset() int64 {
|
|
return int64(h.HeaderLen) + int64(h.RecCount)*int64(h.RecordLen)
|
|
}
|
|
|
|
// FieldDesc represents a 32-byte field descriptor.
|
|
// Layout is byte-identical to Harbour's DBFFIELD.
|
|
type FieldDesc struct {
|
|
Name [11]byte // offset 0 (null-terminated)
|
|
Type byte // offset 11
|
|
Reserved1 [4]byte // offset 12
|
|
Len byte // offset 16
|
|
Dec byte // offset 17
|
|
Flags byte // offset 18
|
|
Counter [4]byte // offset 19 (autoincrement, LE)
|
|
Step byte // offset 23
|
|
Reserved2 [7]byte // offset 24
|
|
HasTag byte // offset 31
|
|
}
|
|
|
|
// ReadFieldDescs reads n field descriptors from a reader.
|
|
func ReadFieldDescs(r io.Reader, n int) ([]FieldDesc, error) {
|
|
fields := make([]FieldDesc, n)
|
|
for i := 0; i < n; i++ {
|
|
if err := binary.Read(r, binary.LittleEndian, &fields[i]); err != nil {
|
|
return nil, fmt.Errorf("read field descriptor %d: %w", i, err)
|
|
}
|
|
}
|
|
return fields, nil
|
|
}
|
|
|
|
// WriteFieldDescs writes field descriptors to a writer.
|
|
func WriteFieldDescs(w io.Writer, fields []FieldDesc) error {
|
|
for i := range fields {
|
|
if err := binary.Write(w, binary.LittleEndian, &fields[i]); err != nil {
|
|
return fmt.Errorf("write field descriptor %d: %w", i, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetName extracts the field name as a trimmed string.
|
|
func (f *FieldDesc) GetName() string {
|
|
n := len(f.Name)
|
|
for i, b := range f.Name {
|
|
if b == 0 {
|
|
n = i
|
|
break
|
|
}
|
|
}
|
|
// Trim trailing spaces (DBF field names are space-padded)
|
|
for n > 0 && f.Name[n-1] == ' ' {
|
|
n--
|
|
}
|
|
return string(f.Name[:n])
|
|
}
|
|
|
|
// SetName sets the field name (max 10 chars, null-terminated, space-padded).
|
|
func (f *FieldDesc) SetName(name string) {
|
|
f.Name = [11]byte{}
|
|
copy(f.Name[:], name)
|
|
}
|
|
|
|
// BuildFieldOffsets calculates the byte offset of each field within a record.
|
|
// offset[0] = 1 (after deletion flag), offset[i+1] = offset[i] + field[i].Len
|
|
// Harbour: pFieldOffset array built during OPEN.
|
|
func BuildFieldOffsets(fields []FieldDesc) []uint16 {
|
|
offsets := make([]uint16, len(fields)+1)
|
|
offsets[0] = 1 // skip deletion flag byte
|
|
for i, f := range fields {
|
|
offsets[i+1] = offsets[i] + uint16(f.Len)
|
|
}
|
|
return offsets
|
|
}
|