Foxi is a Go package for reading FoxPro database tables. It supports both CGO-based and pure Go backends depending on build tags, providing flexibility between performance and deployment simplicity.
- Dual Backend Support: Choose between pure Go or CGO implementation at compile time
- Build Tag Selection: Simple build tag controls backend selection
- Self-Contained: All dependencies are included in subpackages
- Full DBF Support: Complete Visual FoxPro database compatibility
- Type-Safe Field Access: Unified Field API with automatic type conversion
- Source: gomkfdbf - Complete Go translation of CodeBase library
- Advantages: No CGO dependencies, cross-platform builds, static linking
- Build:
go build(default) - Performance: ~70% of C performance with 100% memory safety
- Source: mkfdbf C library - Production-proven CodeBase implementation
- Advantages: Maximum performance, battle-tested codebase
- Build:
go build -tags foxicgo - Requirements: CGO enabled, C compiler
go get github.com/mkfoss/foxiYou can also access the backend packages directly from within the foxi module:
# Get the full foxi package (includes all backends)
go get github.com/mkfoss/foxipackage main
import (
"fmt"
"log"
"github.com/mkfoss/foxi"
)
func main() {
// Create new instance (backend selected by build tags)
f := foxi.NewFoxi()
// Open database
err := f.Open("customers.dbf")
if err != nil {
log.Fatal(err)
}
defer f.Close()
// Show backend information
fmt.Printf("Using backend: %s\n", f.Backend().String())
// Get database information
header := f.Header()
fmt.Printf("Records: %d, Last Updated: %s\n",
header.RecordCount(), header.LastUpdated().Format("2006-01-02"))
// Navigate through records
f.First()
for !f.EOF() {
nameField := f.FieldByName("NAME")
name, _ := nameField.AsString()
ageField := f.FieldByName("AGE")
age, _ := ageField.AsInt()
fmt.Printf("Record %d: %s, Age: %d\n", f.Position(), name, age)
f.Next()
}
}go build # Uses gomkfdbf backend
go run main.go # Pure Go, no CGO requiredgo build -tags foxicgo # Uses mkfdbf C library
CGO_ENABLED=1 go build -tags foxicgo# Pure Go - works on all platforms
GOOS=linux GOARCH=amd64 go build
GOOS=windows GOARCH=amd64 go build
GOOS=darwin GOARCH=arm64 go build
# CGO builds require appropriate toolchain per platform
CGO_ENABLED=1 GOOS=linux go build -tags foxicgof := foxi.NewFoxi() // Create new instance
err := f.Open("data.dbf") // Open database
defer f.Close() // Always close when done
active := f.Active() // Check if database is open
backend := f.Backend() // Get backend informationheader := f.Header()
count := header.RecordCount() // Total records
updated := header.LastUpdated() // Last modification date
hasIndex := header.HasIndex() // Has index files
hasMemo := header.HasFpt() // Has memo file
codepage := header.Codepage() // Character encoding// Field count and enumeration
fieldCount := f.FieldCount()
for i := 0; i < fieldCount; i++ {
field := f.Field(i)
fmt.Printf("%s (%s)\n", field.Name(), field.Type().String())
}
// Access field by name
nameField := f.FieldByName("CUSTOMER_NAME")
if nameField != nil {
value, _ := nameField.AsString()
fieldType := nameField.Type()
size := nameField.Size()
}f.First() // Go to first record
f.Last() // Go to last record
f.Next() // Next record
f.Previous() // Previous record
f.Skip(10) // Skip multiple records
f.Goto(42) // Go to specific record
pos := f.Position() // Current record number (1-indexed)
isEOF := f.EOF() // At end of file
isBOF := f.BOF() // At beginning of filefield := f.FieldByName("SOME_FIELD")
// Type-safe conversion methods
stringVal, err := field.AsString() // Convert to string
intVal, err := field.AsInt() // Convert to integer
floatVal, err := field.AsFloat() // Convert to float64
boolVal, err := field.AsBool() // Convert to boolean
timeVal, err := field.AsTime() // Convert to time.Time
// Native value access
nativeVal, err := field.Value() // Get in native type
isNull, err := field.IsNull() // Check for nulldeleted := f.Deleted() // Check if record is deleted
err := f.Delete() // Mark record for deletion (soft delete)
err := f.Recall() // Undelete recordFoxi provides comprehensive index support with lazy loading for efficient database access:
// Access the indexes collection (lazy loaded)
indexes := f.Indexes()
// List all available indexes
indexList := indexes.List()
fmt.Printf("Available indexes: %d\n", len(indexList))
for _, index := range indexList {
fmt.Printf(" Index: %s, Tags: %d\n", index.Name(), index.TagCount())
}
// List all available tags from all indexes
tags := indexes.Tags()
fmt.Printf("Available tags: %d\n", len(tags))
for _, tag := range tags {
fmt.Printf(" Tag: %s, Expression: %s, KeyLen: %d\n",
tag.Name(), tag.Expression(), tag.KeyLength())
}
// Select a tag for navigation
nameTag := indexes.TagByName("NAME_IDX")
if nameTag != nil {
err := indexes.SelectTag(nameTag)
if err == nil {
// Navigation now follows index order
f.First() // First in index order
f.Next() // Next in index order
}
}
// Return to physical record order
indexes.SelectTag(nil)
// Get currently selected tag
selectedTag := indexes.SelectedTag()
if selectedTag != nil {
fmt.Printf("Using tag: %s\n", selectedTag.Name())
}Use tags to quickly find specific records:
// Get a tag for seeking
indexes := f.Indexes()
nameTag := indexes.TagByName("NAME_IDX")
if nameTag == nil {
log.Fatal("NAME_IDX tag not found")
}
// Seek for specific values
result, err := nameTag.SeekString("Smith")
if err != nil {
log.Printf("Seek failed: %v", err)
} else {
switch result {
case foxi.SeekSuccess:
fmt.Println("Found exact match")
// Record is now positioned at the matching entry
nameField := f.FieldByName("NAME")
name, _ := nameField.AsString()
fmt.Printf("Found: %s at record %d\n", name, f.Position())
case foxi.SeekAfter:
fmt.Println("Positioned after where record would be")
case foxi.SeekEOF:
fmt.Println("Value would be after last record")
}
}
// Seek with different data types
result, err = nameTag.Seek("Smith") // Generic seek
result, err = nameTag.SeekInt(12345) // Integer seek
result, err = nameTag.SeekDouble(50000.0) // Float64 seek
// Tag-specific navigation
if result == foxi.SeekSuccess {
err = nameTag.Next() // Next in tag order
err = nameTag.Previous() // Previous in tag order
key := nameTag.CurrentKey() // Current index key value
recNo := nameTag.RecordNumber() // Current record number
pos := nameTag.Position() // Position as percentage (0.0-1.0)
}The current foxi implementation provides:
β Implemented Features:
- Lazy-loaded index discovery and access
- Index and tag enumeration
- Tag selection for record ordering
- Basic seek operations (SeekString, SeekInt, SeekDouble, generic Seek)
- Tag-based navigation (First, Last, Next, Previous)
- Position-based access (Position, PositionSet)
- Tag properties (Name, Expression, KeyLength, IsUnique, IsDescending)
- Current record information (RecordNumber, CurrentKey, EOF, BOF)
π§ Future Enhancements:
- Full expression evaluation for CurrentKey()
- Advanced seek operations (SeekNext for duplicates)
- Expression-based filtering
- Regex search capabilities
- Compiled expression filters
The basic index functionality is fully operational and provides substantial performance benefits for record navigation and seeking.
For convenience, foxi provides "Must" variants of all navigation and field read operations that panic instead of returning errors:
f := foxi.NewFoxi()
f.MustOpen("customers.dbf") // Panics if file doesn't exist
defer f.Close()
// Navigation Must variants
f.MustFirst() // Panic instead of error
f.MustNext() // Panic instead of error
f.MustGoto(10) // Panic instead of error
// Field Must variants
nameField := f.FieldByName("NAME")
name := nameField.MustAsString() // Panic instead of (value, error)
age := nameField.MustAsInt() // Panic instead of (value, error)
null := nameField.MustIsNull() // Panic instead of (bool, error)
// Index Must variants
indexes := f.Indexes()
indexes.MustLoad() // Panic instead of error
nameTag := indexes.TagByName("NAME_IDX")
if nameTag != nil {
indexes.MustSelectTag(nameTag) // Panic instead of error
result := nameTag.MustSeekString("Smith") // Panic instead of (SeekResult, error)
nameTag.MustFirst() // Panic instead of error
}Must variants are ideal for:
- Rapid prototyping where you want to fail fast
- Scripts where error handling isn't critical
- Cases where you know operations should never fail
- Reducing boilerplate in simple use cases
DBF files use "soft delete" - records are marked for deletion but remain in the file:
// Check if current record is deleted
f.First()
if f.Deleted() {
fmt.Println("Current record is marked for deletion")
}
// Mark record for deletion (soft delete)
err := f.Delete()
if err != nil {
log.Printf("Failed to delete record: %v", err)
}
// Undelete (recall) a record
err = f.Recall()
if err != nil {
log.Printf("Failed to recall record: %v", err)
}
// Additional deleted record operations would be available
// in future versions of foxiFoxi supports all standard DBF field types:
| Type | Code | Description | Go Type |
|---|---|---|---|
| Character | C | Text fields | string |
| Numeric | N | Numbers with decimals | float64 |
| Date | D | Dates (CCYYMMDD) | time.Time |
| Logical | L | Boolean (T/F, Y/N) | bool |
| Integer | I | 32-bit integers | int |
| Float | F | Floating-point | float64 |
| DateTime | T | Date and time | time.Time |
| Currency | Y | Money values | float64 |
| Memo | M | Large text fields | string |
f := foxi.NewFoxi()
// Always check errors
err := f.Open("data.dbf")
if err != nil {
log.Fatalf("Failed to open: %v", err)
}
// Handle navigation errors
err = f.Goto(999999)
if err != nil {
log.Printf("Invalid record: %v", err)
}
// Field conversion errors
field := f.FieldByName("NUMERIC_FIELD")
value, err := field.AsInt()
if err != nil {
log.Printf("Conversion failed: %v", err)
}Run the implementation-agnostic test suite:
# Test with pure Go backend (default)
go test ./tests/
# Test with CGO backend
go test -tags foxicgo ./tests/
# Run benchmarks
go test -bench=. ./tests/
# Test both backends
go test ./tests/ && go test -tags foxicgo ./tests/Performance comparison between backends (approximate):
| Operation | Pure Go | CGO | Ratio |
|---|---|---|---|
| File Opening | ~89ΞΌs | ~33ΞΌs | 2.7x |
| Record Navigation | ~3.6ms | ~2.6ms | 1.4x |
| Field Access | ~18ns | ~10ns | 1.8x |
| Memory Usage | 66KB | 384KB | 0.17x |
The pure Go implementation achieves 50-70% of C performance while providing 100% memory safety and deployment simplicity.
foxi/
βββ foxi.go # Main API interface
βββ foxi_go.go # Pure Go backend (+build !foxicgo)
βββ foxi_cgo.go # CGO backend (+build foxicgo)
βββ go.mod # Module definition
βββ README.md # This file
βββ pkg/ # Internal backend packages
β βββ gocore/ # Pure Go implementation (gomkfdbf library)
β βββ cgocore/ # CGO implementation
β βββ cgocore.go # CGO wrapper
β βββ mkfdbflib/ # Static library and headers
βββ tests/ # Implementation-agnostic tests
βββ foxi_test.go # Test suite
The backend is selected automatically at compile time:
-
Default: Pure Go backend using gomkfdbf
- No special build flags required
- Works on all platforms Go supports
- No CGO dependencies
-
CGO: C library backend using mkfdbf
- Enabled with
-tags foxicgo - Requires CGO_ENABLED=1
- Maximum performance
- Enabled with
- Visual FoxPro: Full compatibility
- dBASE III/IV/V: Complete support
- Clipper: Full compatibility
- FoxPro 2.x: Supported
- Other xBase: Most variants work
If you prefer to use the backends directly without the unified interface:
import "github.com/mkfoss/foxi/pkg/gocore"
// Direct gomkfdbf usage
cb := &gocore.Code4{}
data := gocore.D4Open(cb, "data.dbf")
defer gocore.D4Close(data)
// Navigate and read fields
gocore.D4Top(data)
field := gocore.D4Field(data, "NAME")
value := gocore.F4Str(field)import "github.com/mkfoss/foxi/pkg/cgocore"
// Direct C library usage
// Note: This is a low-level interface requiring C knowledge- Fork the repository
- Create a feature branch
- Make changes with tests
- Test both backends:
go test ./tests/ go test -tags foxicgo ./tests/
- Submit pull request
MIT License - see LICENSE file for details.
Foxi - Flexible DBF access for modern Go applications