FiveTech Support Forums

FiveWin / Harbour / xBase community
Board index FiveWin for Harbour/xHarbour Lean DBF API for Microservices – Inspired by Mr. Rao
Posts: 6983
Joined: Fri Oct 07, 2005 07:07 PM
Lean DBF API for Microservices – Inspired by Mr. Rao
Posted: Tue Feb 03, 2026 12:36 AM

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
//==============================================================//
Posts: 6983
Joined: Fri Oct 07, 2005 07:07 PM
Re: Lean DBF API for Microservices – Inspired by Mr. Rao
Posted: Tue Feb 03, 2026 07:47 AM

OOP vs. Functional approach (for this DB read operation)

OOP-style version (implicit state)

Code (harbour): Select all Collapse
METHOD Read() CLASS TDbf
   LOCAL hRow := {=>}, n, cField

   /*
      Implicit state:
      - which DBF is open?
      - which alias is active?
      - which record is currently selected?
      - was Seek() or GoTo() called before?
   */

   IF ::Eof()
      RETURN hRow
   ENDIF

   // Implicit record identity
   hRow["_recno"]   := ::RecNo()
   hRow["_deleted"] := ::Deleted()

   FOR n := 1 TO ::nFields
      cField := ::aFields[n]

      // Field position is resolved implicitly
      // (often via FieldPos() internally)
      hRow[ cField ] := FieldGet( FieldPos( cField ) )
   NEXT

RETURN hRow

Characteristics (OOP)

  • ❌ Implicit state
  • current record lives inside the object
  • behavior depends on call order
  • ❌ hard to test in isolation
  • ❌ previous calls influence current behavior
  • ❌ FieldPos() often used implicitly inside loops
  • βœ… convenient for interactive desktop workflows

  • ---

    Functional version (db_api)

    Code (harbour): Select all Collapse
    FUNCTION Db_Read( h, nRec )
       LOCAL hRow := {=>}, cFld, nPos
    
       /*
          Explicit state:
          - h    : DB context (alias, metadata, field positions)
          - nRec : exact record number to read
    
          No hidden object state.
          No dependency on previous calls.
       */
    
       // Explicit record selection
       ( h["alias"] )->( DbGoto( nRec ) )
    
       IF ( h["alias"] )->( Eof() )
          RETURN hRow
       ENDIF
    
       // Explicit technical metadata
       hRow["_recno"]   := ( h["alias"] )->( RecNo() )
       hRow["_deleted"] := ( h["alias"] )->( Deleted() )
    
       // Explicit, O(1) field access via hash
       FOR EACH cFld IN hb_HKeys( h["fldPos"] )
          nPos := h["fldPos"][ cFld ]
          hRow[ cFld ] := ( h["alias"] )->( FieldGet( nPos ) )
       NEXT
    
    RETURN hRow

    Characteristics (functional)

    • βœ… No implicit state
    • βœ… all dependencies are visible in the function call
    • βœ… call order does not matter
    • βœ… O(1) field access (no FieldPos() in loops)
    • βœ… easy to test with a simple hash
    • βœ… request-safe and parallel-friendly

    ---

    Direct comparison

    AspectOOPFunctional
    Stateimplicit (SELF)explicit (h, nRec)
    Record selectionprevious callfunction parameter
    Testabilitydifficulteasy
    Parallel safetyriskysafe
    Debuggingcontext requiredlocally understandable
    Microservice fitmediumhigh

    ---

    Key takeaway (one sentence)

    In OOP, behavior depends on object state;
    in the functional version, behavior depends only on the parameters.


    ---
    Code (harbour): Select all Collapse
    FUNCTION Db_Read( h, nRec )
       LOCAL hRow := {=>}, cFld, nPos
    
       /*
          Explicit state:
          - h      : request-scoped DB context (dbf, alias, metadata)
          - nRec   : the record number we want to read
    
          There is NO implicit state here:
          - no object (SELF)
          - no hidden current record
          - no dependency on previous calls
       */
    
       // Explicitly move to the requested record number
       // (no reliance on a previously selected record)
       ( h["alias"] )->( DbGoto( nRec ) )
    
       // If the record does not exist, return an empty hash
       // (safe behavior for services, no exception, no UI)
       IF ( h["alias"] )->( Eof() )
          RETURN hRow
       ENDIF
    
       /*
          Add technical metadata explicitly:
          - _recno   : DBF record identifier (important for update/delete)
          - _deleted : soft-delete flag (DBF reality)
    
          These values are not hidden in object state,
          they are returned as part of the result.
       */
       hRow["_recno"]   := ( h["alias"] )->( RecNo() )
       hRow["_deleted"] := ( h["alias"] )->( Deleted() )
    
       /*
          Iterate over all fields using a precomputed hash:
    
          h["fldPos"] maps:
             FIELDNAME -> FIELD POSITION
    
          This avoids FieldPos() calls inside loops
          and guarantees O(1) access.
    
          Again: no implicit lookup, everything is explicit.
       */
       FOR EACH cFld IN hb_HKeys( h["fldPos"] )
    
          // Get field position from hash (no linear search)
          nPos := h["fldPos"][ cFld ]
    
          // Read the raw DBF value
          // No type conversion, no JSON logic, no side effects
          hRow[ cFld ] := ( h["alias"] )->( FieldGet( nPos ) )
    
       NEXT
    
       /*
          Return a plain data structure (hash).
    
          The caller decides:
          - how to serialize it
          - how to convert dates
          - how to handle UTF-8
          - how to build JSON
    
          This keeps persistence logic clean and testable.
       */
    RETURN hRow


    This function avoids implicit state by making
    record selection, field access, and context fully explicit.

    Posts: 6983
    Joined: Fri Oct 07, 2005 07:07 PM
    Re: Lean DBF API for Microservices – Inspired by Mr. Rao
    Posted: Tue Feb 03, 2026 11:45 PM

    Absolutely β€” and that’s a very important step.
    If db_api should stand on its own, the terminology must be Harbour-native, neutral, and understandable without Harbourino.

    Below is a clean, English naming and terminology guideline that works with or without Harbourino.


    ---

    DB_API Naming & Terminology Guidelines (English)

    These rules define shared language, not syntax sugar.
    They apply even if no Harbourino blocks are used.


    ---

    1. Database Hash

    A database hash is the hash returned by DbCtx_New().

    Code (harbour): Select all Collapse
    hDB := DbCtx_New( cDbf, cAlias )

    Definition

    A database hash is a structured hash that describes how a database is accessed and controlled.

    It is:

    • not a table
    • not a record
    • not an alias
    • not a connection

    It is a context object implemented as a hash.


    ---

    2. Variable Prefix: hDB*

    All database hashes must use the hDB prefix.

    Code (harbour): Select all Collapse
    hDB_Beg
    hDB_Adr
    hDB_Knd

    This makes it immediately clear:

    This variable controls a database.


    ---

    3. Record Hash

    A record hash is the hash returned by Db_Read().

    Code (harbour): Select all Collapse
    hRow := Db_Read( hDB, nRec )

    Recommended prefixes:

    • hRow
    • hRec

    Never hDB.


    ---

    4. Physical Database Path

    Physical file paths are strings, never contexts.

    Code (harbour): Select all Collapse
    cDbfBeg := cDataPath + "BELEGUNG.DBF"

    Recommended prefixes:

    • cDbf
    • cPath
    • cFile

    ---

    5. DB_API Function Namespace

    All low-level database functions belong to the Db_* namespace.

    Code (harbour): Select all Collapse
    DbCtx_New()
    Db_Open()
    Db_Read()
    Db_Seek()
    Db_Close()

    Rule

    Db_* functions perform database operations only.
    They contain no business logic.


    ---

    6. Database Context (Conceptual Term)

    The term database context is the conceptual name for what the database hash represents.

    Database context describes the logical access scope of a database during a request.

    In code, this is always represented by a database hash.


    ---

    7. Alias Is an Internal Detail

    Alias names:

    • exist only inside the database hash
    • are set in DbCtx_New()
    • must not influence variable naming
    Code (harbour): Select all Collapse
    DbCtx_New( cDbf, "BEG" )

    The alias is not part of the public API.


    ---

    8. Read-Only Is an Access Contract

    readonly expresses intent, not database mechanics.

    Code (harbour): Select all Collapse
    hDB["readonly"] := .T.

    Meaning:

    This context must not modify data.

    This rule applies regardless of DB engine.


    ---

    9. Explicit Is Preferred Over Implicit

    Database access should always reference the database hash explicitly.

    Code (harbour): Select all Collapse
    Db_Read( hDB, nRec )
    Db_Seek( hDB, uKey )

    Implicit global state (selected workarea) should not be relied upon by DB_API users.


    ---

    10. Terminology Must Be Self-Explaining

    If a term needs explanation every time, it is the wrong term.

    Good:

    Code (text): Select all Collapse
    database hash
    database context
    record hash

    Avoid:

    Code (text): Select all Collapse
    handle
    pointer
    magic object

    ---

    One sentence that defines the philosophy

    DB_API uses explicit database contexts represented as hashes to keep database access predictable, testable, and independent of global state.


    ---

    Continue the discussion