ποΈ 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
DBFCDXRDD. 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..
Navigation¶
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
Search¶
// 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
nTotalPagesrounding up. - If
nPage > nTotalPages, it will relocate to the last page. - Uses
OrdKeyGotoif there's an active index,DbGotoif 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¶
Rlock() retries up to nTime seconds (default 3s) before failing.
If it fails, it calls SetError( DBF_ERR_LOCK ) and returns .F..
Update()andInsert()already manage lock/unlock - you only need to callRlock()directly when you do manualFieldPutoperations.
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:
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
.prgwithREQUEST 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¶
- One model
TXxx()per table. EncapsulatescPath,cDbf,cCdx,cTag,Hide/Visiblein a single reusable function. - 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. Hidesensitive fields. Salaries, passwords, internal keys should never reach a template or JSON.lToStringWeb = .T.for forms. Avoids xBase strings with padding spaces or dates with local formatting.- Combine with the validator.
oVal:DataFields()βoDbf:Update()is the direct pattern, with no ad-hoc code. Updateonly changed keys. Pass only the modified fields in the hash -Update()iterates through hash keys, doesn't touch others.Rlockis not eternal. Default 3s - raiseoDbf:nTimeif your app has high concurrency, or decide to retry at the controller level.