Skip to content

πŸ—„οΈ UDbf - DBF table access

What is it?

UDbf() is the wrapper that HixStyle exposes over the HIX_DBF class to talk to DBF + CDX files (xBase / Clipper / Harbour) from your controllers as if they were a modern data model.

It encapsulates the xBase mechanics (alias, DbGoTo, DbSeek, Rlock, FieldGet) behind a hash-oriented API - records go in and out as dictionaries { "field" => value }, ready to take to JSON, a template, or a validator.

www/models/tcustomers.prg     ──▢  returns configured and open oDbf
                                       β”‚
controllers/customer.prg      ──┐      β”‚
   nId := UGetResource()        β”‚      β”‚
   TCustomers():GetRecno(nId, @hRow, NIL, .T.)
                                β”‚
                                β–Ό
                          hRow = { "first" => "Carles",
                                   "last"  => "Aubia",
                                   "city"  => "Barcelona",
                                   "_recno" => 42,
                                   "_deleted" => .F. }

It's the Fenix Model pattern: one TXxx() per table, controllers only call methods.


When to use it

Use case UDbf
Management app on existing DBF/CDX files βœ… Yes - canonical pattern
Progressive migration from classic Clipper/Harbour βœ… Yes
Reporting on historical data in DBF βœ… Yes
New app on PostgreSQL / MySQL ❌ No - use the SQL adapter
Data in JSON / NoSQL ❌ No
Cache / configuration in memory ❌ No - use the cache system

UDbf is not an ORM. It's a direct wrapper over the DBFCDX RDD. It doesn't generate SQL, doesn't migrate schemas, doesn't resolve relationships; it stays at the classic xBase abstraction.


Creating a model

The Fenix convention: one .prg per table in www/models/ that returns the configured and open instance.

// www/models/tcustomers.prg
FUNCTION TCustomers()
   LOCAL oCustomers := UDbf()

   oCustomers:cPath := hb_dirbase() + "data"
   oCustomers:cDbf  := "customers.dbf"
   oCustomers:cCdx  := "customers.cdx"
   oCustomers:cTag  := "first"

   oCustomers:Hide( "salary" )    // this field must not reach hRow
   oCustomers:Open()

RETURN oCustomers
// www/models/tstates.prg
FUNCTION TStates()
   LOCAL oStates := UDbf()

   oStates:cPath := hb_dirbase() + "data"
   oStates:cDbf  := "states.dbf"
   oStates:cCdx  := "states.cdx"
   oStates:cTag  := "name"

   oStates:Open()
RETURN oStates

The constructor also accepts the arguments on a single line: UDbf( "customers.dbf", "customers.cdx", "first", NIL, .T. ). The Fenix pattern prefers to assign properties for clarity.


Properties

Property Default Purpose
cPath hb_dirbase() Folder where the .dbf and .cdx are located
cDbf "" Name of the .dbf file
cCdx "" Name of the .cdx index file
cTag "" Active tag when opening
cRdd "DBFCDX" RDD (DBFNTX, DBFCDX, ...)
lExclusive .F. Open in exclusive mode (multi-user β†’ .F.)
lToUtf8 .F. Convert strings to UTF-8 when reading
cAlias (autogenerated) Alias generated by NewAlias()
hFields {=>} Structure: name β†’ {name, type, len, dec}
nFields (autocalc) Number of visible fields
lConnect .F. .T. if the table is open

Open and close

oDbf:Open()              // opens and loads structure β†’ ::lConnect = .T.
oDbf:Close()             // closes the area

oDbf:lExclusive := .T.
oDbf:Open()              // exclusive (Zap, Pack, repair)

Open() returns .T. if it opens successfully. If the file is missing or the tag doesn't exist, it calls SetError() and returns .F..


oDbf:First()             // DbGoTop
oDbf:Last()              // DbGoBottom
oDbf:Next()              // DbSkip(1)
oDbf:Prev()              // DbSkip(-1)
oDbf:Skip( 5 )           // DbSkip(5)
oDbf:Goto( nRecno )      // DbGoTo(nRecno)

oDbf:Recno()             // current record number
oDbf:RecCount()          // total records
oDbf:Bof() / oDbf:Eof()  // boundaries

// Seek by the active index key
IF oDbf:Seek( "Carles" )
   ? "Found at recno " + Str( oDbf:Recno() )
ENDIF

// Seek in another index without losing the current one
oDbf:Seek( "1234", .F., "id" )

// Change active index
oDbf:Focus( "city" )

Read a record as a hash

Row() is the heart of the wrapper: it converts the current record into a hash ready to use.

hRow := oDbf:Row()                       // all visible fields
hRow := oDbf:Row( { "first", "last" } )  // only those fields
hRow := oDbf:Row( NIL, .T. )             // converted to web string

The hash always adds two control fields:

Key Value
_recno Physical record number
_deleted .T. / .F. (deletion mark)

Web string mode

When lToStringWeb = .T., values are serialized to string ready to put in an <input value="...">:

DBF Type Result
C, M AllTrim(value) (optionally UTF-8 if lToUtf8)
D UDateToHtml( dValue ) β†’ "2026-06-26"
N Str( value, len, dec )
L ULogicToHtmlChecked( value ) β†’ "checked" / ""

It's the mode that controllers use to fill HTML forms.


Basic CRUD

Create - Insert

LOCAL hFields := { ;
   "first"  => "Carles",  ;
   "last"   => "Aubia",   ;
   "city"   => "Barcelona" ;
}
LOCAL cError, nNewRecno

IF oDbf:Insert( hFields, @cError, @nNewRecno )
   ? "Created recno " + Str( nNewRecno )
ELSE
   ? "Error: " + cError
ENDIF

Insert does Append() + Update() in a single call.

Read - GetRecno / GetId

LOCAL hRow := {=>}

// By physical recno
IF oDbf:GetRecno( 42, @hRow, NIL, .T. )
   ? hRow[ "first" ], hRow[ "city" ]
ENDIF

// By active index key
IF oDbf:GetId( "Carles", @hRow )
   ? hRow[ "_recno" ]
ENDIF

Both return .T. if the record exists and fill @hRow by reference. The fourth parameter (lToStringWeb) is the same as Row().

Update - Update

LOCAL hChanges := { "city" => "Madrid", "salary" => 50000 }
LOCAL cError

IF oDbf:Update( 42, hChanges, @cError )
   ? "Updated"
ELSE
   ? "Error: " + cError
ENDIF

Update does Rlock β†’ FieldPut for each hash key β†’ DbCommit β†’ DbUnlock.

Delete - Delete

oDbf:Delete( nRecno )                      // marks as deleted
oDbf:Delete( nRecno, .T. )                 // toggle: if deleted, recover it
oDbf:Delete( nRecno, .F., @lIsDeleted )    // @lIsDeleted with the final state

oDbf:Recall()                              // remove deletion mark (current record)
oDbf:Pack( @cError )                       // physically remove deleted rows
oDbf:Zap()                                 // empty the table - ⚠️ exclusive

Blank record for forms

hBlank := oDbf:Blank()              // hash with empty values by type
hBlank := oDbf:Blank( .T. )         // web string (for Create form)

Useful when painting a creation form: the template reads from the same hash that a form edit will use.


List

All records

aRows := oDbf:LoadAll()                            // all visible fields
aRows := oDbf:LoadAll( { "first", "city" } )       // only those
aRows := oDbf:LoadAll( NIL, "A", "C" )             // scope: from "A" to "C"
aRows := oDbf:LoadAll( NIL, , , {|a| !Deleted() } ) // with a codeblock condition

Returns an array of hashes (one per record). Applies OrdScope if cScopeTop/cScopeBottom are passed.

Pagination

LOCAL nTotalPages
LOCAL aRows := oDbf:Page( 1, 20, NIL, @nTotalPages )

// nTotalPages returned by reference
? "Page 1/" + Str( nTotalPages ) + " - " + Str( Len( aRows ) ) + " rows"

Page( nPage, nRows, aFields, @nTotalPages ):

  • Calculates nTotalPages rounding up.
  • If nPage > nTotalPages, it will relocate to the last page.
  • Uses OrdKeyGoto if there's an active index, DbGoto if not.

Field visibility

Useful for hiding sensitive fields (salary, password) from the hash that travels to templates / JSON:

oDbf:Hide( "salary" )                      // single field
oDbf:Hide( { "salary", "ssn", "passwd" } ) // multiple

oDbf:Visible( { "id", "first", "last" } )  // whitelist: only these
Method Behavior
Hide( aFields ) Blacklist - all except those
Visible( aFields ) Whitelist - only those

Apply before Open(). The hFields structure is trimmed when opening according to the selection.


Locking

IF oDbf:Rlock()
   oDbf:FieldPut( "city", "Madrid" )
   oDbf:Unlock()
ENDIF

Rlock() retries up to nTime seconds (default 3s) before failing. If it fails, it calls SetError( DBF_ERR_LOCK ) and returns .F..

Update() and Insert() already manage lock/unlock - you only need to call Rlock() directly when you do manual FieldPut operations.


Complete Fenix pattern - Customer Edit

The controller calls the model

METHOD Edit() CLASS Customer
   LOCAL oVal := UValidateParams( { "id" => { "required|number|min:0", "Id" } } )
   LOCAL oCustomers, oStates, hRow := {=>}
   LOCAL aStates, lFound

   IF ! oVal:Make()
      RETURN URedirect( URoute( "customer.search" ) )
   ENDIF

   oStates := TStates()                    // ← UDbf for states
   aStates := oStates:LoadAll()

   oCustomers := TCustomers()              // ← UDbf for customers
   lFound := oCustomers:GetRecno( oVal:Get( "id" ), @hRow, NIL, .T. )

   IF ! lFound
      hRow := oCustomers:Blank( .T. )      // blank for "create"
   ENDIF

RETURN UView( "masters/customer/edit.html", "edit", lFound, hRow, aStates )

Update

METHOD Update() CLASS Customer
   LOCAL cId := UGetResource()
   LOCAL oVal, oCustomers, lSuccess, cError

   oVal := UValidatePost( { ;
      "first" => "required|string|max:20|field", ;
      "city"  => "required|string|max:30|field", ;
      "age"   => "required|numeric|max:99|field" ;
   } )

   IF ! oVal:Make()
      UFlash( "customer" ):Set( { "errors" => oVal:GetErrors(), "input" => oVal:Resume() } )
      RETURN URedirect( URoute( "customer.edit", Val( cId ) ) )
   ENDIF

   oCustomers := TCustomers()
   lSuccess := oCustomers:Update( Val( cId ), oVal:DataFields(), @cError )

   IF lSuccess
      UFlash( "customer" ):Set( { "message" => "Updated" } )
      RETURN URedirect( URoute( "customer.show", Val( cId ) ) )
   ELSE
      UFlash( "customer" ):Set( { "message" => cError } )
      RETURN URedirect( URoute( "customer.edit", Val( cId ) ) )
   ENDIF
RETURN

Notice the symbiosis with the validator:

Helper Returns
oVal:DataFields() Only keys marked with field in the rules
oVal:Resume() All the original input (to repaint the form)

DataFields is designed to directly feed oDbf:Update().


UTF-8 and encoding

Classic DBF files are usually in CP437 or CP850. So that strings arrive as UTF-8 in the browser:

oDbf:lToUtf8 := .T.
oDbf:Open()

With lToUtf8 = .T., Row() applies hb_StrToUtf8() to C and M fields before returning them.

If you activate it, always write from UTF-8 or you'll have broken characters. Best: properly configure Harbour's codepage in the main .prg with REQUEST HB_CODEPAGE_* before touching anything.


Errors

UDbf captures xBase errors in TRY/CATCH and reroutes them to HIX_Throw, which the HIX dispatcher catches to paint the corresponding error page.

oDbf:lDoError := .F.    // disable automatic throw
oDbf:Open()
IF ! oDbf:lConnect
   ? "Error: " + oDbf:oError:description
ENDIF

Best practices

  1. One model TXxx() per table. Encapsulates cPath, cDbf, cCdx, cTag, Hide/Visible in a single reusable function.
  2. Close what you open. In HTTP workers, the alias lives in the thread pool. Use Close() at the end of the request or rely on GC of the area ID per thread.
  3. Hide sensitive fields. Salaries, passwords, internal keys should never reach a template or JSON.
  4. lToStringWeb = .T. for forms. Avoids xBase strings with padding spaces or dates with local formatting.
  5. Combine with the validator. oVal:DataFields() β†’ oDbf:Update() is the direct pattern, with no ad-hoc code.
  6. Update only changed keys. Pass only the modified fields in the hash - Update() iterates through hash keys, doesn't touch others.
  7. Rlock is not eternal. Default 3s - raise oDbf:nTime if your app has high concurrency, or decide to retry at the controller level.