Objective:
A minimalist persistence layer for microservices, adopting proven concepts from TDataBase (Mr. Rao) while deliberately omitting desktop-specific features.
Adopted Concepts:
Field Metadata as Single Source of Truth (types, lengths, memo fields)
Hash-based FieldPos Access (O(1) complexity, critical under load)
Explicit Locking (no hidden commits, short lock durations)
Simplified Triggers/Hooks (for Insert/Update/Delete)
Omitted Features:
UI dependencies (MsgBox, XBrowse)
Global state (relations, filters, scopes)
Buffer/dirty-tracking across requests
Implicit type conversion or defaults
Why Lean?
Microservices demand control, not convenience. The API is restricted to:
Open/Close, CRUD, Seek, Locking, Metadata
Everything else (validation, business logic, JSON/UTF-8) belongs in higher layers.
Why Functions Over OOP?
Request-based: No natural object lifecycle as in GUIs.
Explicit State: Each call is isolated, testable, and auditable.
No Hidden Dependencies: DBF workareas and aliases are managed intentionally.
Scalability: Functions are deterministic, easily mockable, and team-friendly.
Conclusion:
Not a competitor to TDataBase, but a purpose-built derivative for server contexts.
//==============================================================//
// test_db_api.prg
// Slim, generic DBF persistence API (functional style)
// For Microservices / Server / Batch jobs
//
// Goals:
// - NO classes
// - NO UI dependencies
// - NO implicit global state
// - Request / context based
// - Fast FieldPos() via hash (O(1))
// - Explicit locking, insert, update, delete, read
// - Optional trigger hooks (before / after)
//
// Inspired by ideas from Mr. Rao (TDataBase) and real-world microservice usage
//==============================================================//
#include "fivewin.ch"
#include "fileio.ch"
#include "dbinfo.ch"
REQUEST DBFCDX
FUNCTION MAIN
? "Start"
DbApi_Test()
? "Ende"
RETURN NIL
//--------------------------------------------------------------//
// Create a DB context (request-scoped)
//--------------------------------------------------------------//
FUNCTION DbCtx_New( cDbf, cAlias, lShared, lReadOnly )
LOCAL h := {=>}
hb_default( @cAlias, "DB" )
hb_default( @lShared, .T. )
hb_default( @lReadOnly, .F. )
h["dbf"] := cDbf
h["alias"] := cAlias
h["shared"] := lShared
h["readonly"] := lReadOnly
// Metadata
h["struct"] := {}
h["fldPos"] := {=>} // field name -> field position
h["hasMemo"] := .F.
// Optional trigger hooks
h["beforeInsert"] := NIL
h["afterInsert"] := NIL
h["beforeUpdate"] := NIL
h["afterUpdate"] := NIL
h["beforeDelete"] := NIL
h["afterDelete"] := NIL
RETURN h
//--------------------------------------------------------------//
// Open DBF and load metadata
//--------------------------------------------------------------//
FUNCTION Db_Open( h )
LOCAL n
IF ! File( h["dbf"] )
RETURN .F.
ENDIF
USE ( h["dbf"] ) SHARED NEW ALIAS ( h["alias"] )
IF NetErr()
RETURN .F.
ENDIF
h["struct"] := ( h["alias"] )->( DbStruct() )
FOR n := 1 TO Len( h["struct"] )
h["fldPos"][ h["struct"][n,1] ] := n
IF h["struct"][n,2] == "M"
h["hasMemo"] := .T.
ENDIF
NEXT
RETURN .T.
//--------------------------------------------------------------//
// Close DBF
//--------------------------------------------------------------//
FUNCTION Db_Close( h )
IF Select( h["alias"] ) > 0
( h["alias"] )->( DbCloseArea() )
ENDIF
RETURN NIL
//--------------------------------------------------------------//
// Read a record -> hash
//--------------------------------------------------------------//
FUNCTION Db_Read( h, nRec )
LOCAL hRow := {=>}, cFld, nPos
( h["alias"] )->( DbGoto( nRec ) )
IF ( h["alias"] )->( Eof() )
RETURN hRow
ENDIF
hRow["_recno"] := ( h["alias"] )->( RecNo() )
hRow["_deleted"] := ( h["alias"] )->( Deleted() )
FOR EACH cFld IN hb_HKeys( h["fldPos"] )
nPos := h["fldPos"][ cFld ]
hRow[ cFld ] := ( h["alias"] )->( FieldGet( nPos ) )
NEXT
RETURN hRow
//--------------------------------------------------------------//
// Insert record
//--------------------------------------------------------------//
FUNCTION Db_Insert( h, hFields, cErr )
LOCAL nRec, cFld, nPos
IF h["readonly"]
cErr := "readonly"
RETURN 0
ENDIF
IF h["beforeInsert"] != NIL
IF ! Eval( h["beforeInsert"], hFields )
cErr := "beforeInsert_failed"
RETURN 0
ENDIF
ENDIF
( h["alias"] )->( DbAppend() )
IF NetErr()
cErr := "append_failed"
RETURN 0
ENDIF
nRec := ( h["alias"] )->( RecNo() )
IF ! ( h["alias"] )->( DbRLock() )
cErr := "lock_failed"
RETURN 0
ENDIF
FOR EACH cFld IN hb_HKeys( hFields )
IF hb_HHasKey( h["fldPos"], cFld )
nPos := h["fldPos"][ cFld ]
( h["alias"] )->( FieldPut( nPos, hFields[cFld] ) )
ENDIF
NEXT
( h["alias"] )->( DbUnlock() )
IF h["afterInsert"] != NIL
Eval( h["afterInsert"], nRec )
ENDIF
RETURN nRec
//--------------------------------------------------------------//
// Update record
//--------------------------------------------------------------//
FUNCTION Db_Update( h, nRec, hFields, cErr )
LOCAL cFld, nPos
IF h["readonly"]
cErr := "readonly"
RETURN .F.
ENDIF
( h["alias"] )->( DbGoto( nRec ) )
IF ( h["alias"] )->( Eof() )
cErr := "not_found"
RETURN .F.
ENDIF
IF h["beforeUpdate"] != NIL
IF ! Eval( h["beforeUpdate"], nRec, hFields )
cErr := "beforeUpdate_failed"
RETURN .F.
ENDIF
ENDIF
IF ! ( h["alias"] )->( DbRLock() )
cErr := "lock_failed"
RETURN .F.
ENDIF
FOR EACH cFld IN hb_HKeys( hFields )
IF hb_HHasKey( h["fldPos"], cFld )
nPos := h["fldPos"][ cFld ]
( h["alias"] )->( FieldPut( nPos, hFields[cFld] ) )
ENDIF
NEXT
( h["alias"] )->( DbUnlock() )
IF h["afterUpdate"] != NIL
Eval( h["afterUpdate"], nRec )
ENDIF
RETURN .T.
//--------------------------------------------------------------//
// Delete record (soft delete via DBDELETE)
//--------------------------------------------------------------//
FUNCTION Db_Delete( h, nRec, cErr )
IF h["readonly"]
cErr := "readonly"
RETURN .F.
ENDIF
( h["alias"] )->( DbGoto( nRec ) )
IF ( h["alias"] )->( Eof() )
cErr := "not_found"
RETURN .F.
ENDIF
IF h["beforeDelete"] != NIL
IF ! Eval( h["beforeDelete"], nRec )
cErr := "beforeDelete_failed"
RETURN .F.
ENDIF
ENDIF
IF ! ( h["alias"] )->( DbRLock() )
cErr := "lock_failed"
RETURN .F.
ENDIF
( h["alias"] )->( DbDelete() )
( h["alias"] )->( DbUnlock() )
IF h["afterDelete"] != NIL
Eval( h["afterDelete"], nRec )
ENDIF
RETURN .T.
//--------------------------------------------------------------//
// Seek (index-based)
//--------------------------------------------------------------//
FUNCTION Db_Seek( h, uKey, lSoft )
hb_default( @lSoft, .F. )
RETURN ( h["alias"] )->( DbSeek( uKey, lSoft ) )
//--------------------------------------------------------------//
// Read all records -> array of hashes
//--------------------------------------------------------------//
FUNCTION Db_ReadAll( h )
LOCAL a := {}, hRow
( h["alias"] )->( DbGoTop() )
DO WHILE ! ( h["alias"] )->( Eof() )
hRow := Db_Read( h, ( h["alias"] )->( RecNo() ) )
AAdd( a, hRow )
( h["alias"] )->( DbSkip() )
ENDDO
RETURN a
//==============================================================//
// Simple test / demo entry point
// Run: harbour db_api.prg test.prg
//==============================================================//
FUNCTION DbApi_Test()
LOCAL cDbf := "mytest.dbf"
LOCAL hDb, nRec, cErr := ""
LOCAL hRow
// Create test DBF if missing
IF ! File( cDbf )
DbCreate( cDbf, { ;
{ "ID", "N", 6, 0 }, ;
{ "NAME", "C", 20, 0 }, ;
{ "DATE", "D", 8, 0 } ;
}, "DBFCDX" )
ENDIF
hDb := DbCtx_New( cDbf )
IF ! Db_Open( hDb )
? "Could not open DBF"
RETURN NIL
ENDIF
// Insert
nRec := Db_Insert( hDb, { ;
"ID" => 1, ;
"NAME" => "Hello World", ;
"DATE" => Date() ;
}, @cErr )
? "Inserted recno:", nRec, "error:", cErr
// Read
hRow := Db_Read( hDb, nRec )
? "Read row:", hb_ValToExp( hRow )
// Update
Db_Update( hDb, nRec, { "NAME" => "Updated" }, @cErr )
? "After update:", hb_ValToExp( Db_Read( hDb, nRec ) )
// Delete
Db_Delete( hDb, nRec, @cErr )
? "Deleted flag:", Db_Read( hDb, nRec )["_deleted"]
Db_Close( hDb )
RETURN NIL
//==============================================================//
// End of db_api.prg
//==============================================================//