Lean DBF API for Microservices – Inspired by Mr. Rao

Post Reply
User avatar
Otto
Posts: 7020
Joined: Fri Oct 07, 2005 7:07 pm
Contact:

Lean DBF API for Microservices – Inspired by Mr. Rao

Post by Otto »

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.

Code: Select all | Expand


//==============================================================//
// 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
//==============================================================//


User avatar
Otto
Posts: 7020
Joined: Fri Oct 07, 2005 7:07 pm
Contact:

Re: Lean DBF API for Microservices – Inspired by Mr. Rao

Post by Otto »

OOP vs. Functional approach (for this DB read operation) OOP-style version (implicit state)

Code: Select all | Expand

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: Select all | Expand

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: Select all | Expand

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.

User avatar
Otto
Posts: 7020
Joined: Fri Oct 07, 2005 7:07 pm
Contact:

Re: Lean DBF API for Microservices – Inspired by Mr. Rao

Post by Otto »

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: Select all | Expand

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: Select all | Expand

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: Select all | Expand

hRow := Db_Read( hDB, nRec )

Recommended prefixes:

  • hRow
  • hRec

Never hDB.

--- 4. Physical Database Path

Physical file paths are strings, never contexts.

Code: Select all | Expand

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: Select all | Expand

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: Select all | Expand

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: Select all | Expand

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: Select all | Expand

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: Select all | Expand

database hash
database context
record hash

Avoid:

Code: Select all | Expand

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.

---
Post Reply