Building Database Drivers¶
Data Dictionaries talk to database storage through adapter objects. Build a driver when the needed default driver does not exist yet, or when an application should talk to a web service through the same structured Data Dictionary model instead of ad hoc service calls.
Use Connections to register and select drivers, Entities to describe tables and fields, Data Dictionaries to coordinate validation and record operations, and Data Binding to bind controls to Data Dictionary buffers. Use cEntity for metadata inspection and cDataDictionary for Data Dictionary behavior.
Reference pages: DBP Types, Driver/Adapter Protocol, and cDbSqliteDriver.
The driver authoring contract is the Data Dictionary adapter message format. It is not direct UI binding and it is not direct table-buffer access.
When to build a driver¶
Build a driver when storage must be accessed through a backend that is not already available through an existing connection scheme, or when a remote service should behave like a Data Dictionary-backed data source. Common cases are a local SQL engine, a service gateway, or a backend with its own query language.
A driver sits behind a connection URI. Data Dictionaries and entities keep the application model stable while the driver translates adapter messages into storage-specific create, find, update, delete, query, and schema operations.
Driver object shape¶
A driver is an object that imports the adapter mixin, exposes connection properties, initializes a connection URI, reports itself as an adapter, and processes tDbMessage requests.
Required adapter members:
psConnectionIdpsConnectionUriInitializeConnection String sConnectionURI Returns BooleanIsAdapter Returns BooleanProcessMessage tDbMessage ByRef msg
Use Db\cDbFlexAdapter_mixin.pkg
Class cMyDbDriver is a cObject
Procedure Construct_Object
Forward Send Construct_Object
Send Define_cDbFlexAdapter_mixin
End_Procedure
Import_Class_Protocol cDbFlexAdapter_mixin
Function InitializeConnection String sConnectionURI Returns Boolean
// Parse and open sConnectionURI here.
Function_Return True
End_Function
Procedure FindRecord tDbMessage ByRef msg
// Read msg.request as tDbReqFind and write msg.response as tDbResFind.
End_Procedure
End_Class
A driver should import the mixin and override only the operations it supports. Unsupported operations should fail through the mixin's default DFERR_PROGRAM "Not supported" behavior rather than silently succeeding.
Message envelope¶
ProcessMessage receives a tDbMessage envelope. The envelope identifies the operation, carries operation-specific request and response variants, and records lifecycle state.
| Field | Driver meaning |
|---|---|
eOperation |
Operation selector. Dispatch values are listed in the next section. |
hoModel |
Data model / DD object that exposes metadata helpers such as TableDefinition, TableFieldCount, and field naming. |
eState |
Message lifecycle state. Driver sets C_DbMsgState_Finished or C_DbMsgState_Failed. |
iErrNr |
Driver-specific error number when failed. |
sError |
Driver-facing error text when failed. |
aStates |
Operation context: constraints, relations, main table, and involved tables. |
request |
Operation-specific request struct stored as a Variant. |
response |
Operation-specific response struct stored as a Variant. |
Lifecycle values:
C_DbMsgState_NewC_DbMsgState_PendingC_DbMsgState_FailedC_DbMsgState_Finished
request and response are variants. The driver casts or moves them to the operation-specific structs listed below.
Operation messages¶
Each eOperation value dispatches to a driver method with a matching request and response shape.
eOperation |
Driver method | Request struct | Response struct |
|---|---|---|---|
C_DbMsg_Create |
CreateRecord |
tDbReqCreate |
tDbResCreate |
C_DbMsg_Find |
FindRecord |
tDbReqFind |
tDbResFind |
C_DbMsg_Update |
UpdateRecord |
tDbReqUpdate |
tDbResUpdate |
C_DbMsg_Delete |
DeleteRecord |
tDbReqDelete |
none |
C_DbMsg_Query |
QueryRecords |
tDbReqQuery |
tDbResQuery |
C_DbMsg_CreateDb |
CreateDb |
tDbReqCreateDb |
none |
C_DbMsg_Exists |
CheckTablesExist |
tDbReqExists |
tDbResExists |
Required fields by operation:
tDbReqFind:aIndex,aOrder,eMode,hMainTable,aParents; responsetDbResFindreturnsbFoundplusaData.tDbReqCreate:hMainTable,vValues; response returns onetDbTableData record.tDbReqUpdate:hMainTable,aConstraints,bReturnRecord,aData; response returns affected row count and optional returned record.tDbReqDelete:hMainTable,aConstraints,bZeroFile.tDbReqCreateDbandtDbReqExists:aTables; create also receivesbOverwrite.tDbReqQuery:hMainTable,eMode,rStartAt,aFields,aOrder; response returns row IDs and tabular data.
For successful operations that produce records, return tDbTableData with hTable, rRowId, and vData filled so the Data Dictionary buffer can be refreshed.
Query and constraint fields¶
Drivers should use table handles, field numbers, metadata structs, and constraints from the message payload instead of hard-coded storage names.
| Type / struct | Meaning |
|---|---|
DbTable |
Table handle (Handle). |
DbField |
Field number (Integer). |
tDbTableDefinition |
Table name, fields, relations, and table attributes. |
tDbFieldDefinition |
Field name, DataFlex field type, size, and field attributes. |
tDbRelation |
Related table and related/foreign key fields. |
tDbConstraint |
Driver-understood constraint with eType, hTable, hField, and vValue. |
Supported constraint constants:
C_DbConstraint_LTC_DbConstraint_LEC_DbConstraint_EQC_DbConstraint_GEC_DbConstraint_GTC_DbConstraint_NEC_DbConstraint_MatchesC_DbConstraint_ContainsC_DbConstraint_RowIdC_DbConstraint_NativeC_DbConstraint_LimitC_DbConstraint_Offset
Drivers should inspect entity metadata through TableDefinition, TableFieldCount, TablePrimaryKeyFields, FieldName, and field/table attributes instead of hard-coding names.
Connection URI handling¶
RegisterDriver "sqlite" (RefClass(cDbSqliteDriver)) binds a URI scheme to a driver class. After registration, a connection URI that starts with sqlite: selects that driver.
sqlite:OrderEntryFull.db?mode=ro
sqlite:OrderEntryFull.db?mode=rwc
sqlite:./data/OrderEntry.db?autocreate=true
sqlite:///C:\SQLiteDB\Stuff\OrderEntry.db?autocreate=true
InitializeConnection receives the full connection URI. It is responsible for parsing driver-specific query parameters, opening or reusing the connection, and reporting connection errors. Use URI parser APIs when the driver needs structured URI parsing.
Local SQL driver example¶
A SQLite-style local driver uses the Data Dictionary adapter API on the DataFlex side and a native SQLite/C module boundary underneath. The driver package exposes native entry points through External_Function declarations, then wraps those calls in small driver methods so adapter operations do not depend on C details.
Typical flow:
InitializeConnectionparses thesqlite:URI, calls the native open function through anExternal_Functionwrapper, and stores the returned database handle on the driver object.CreateDbreadstDbReqCreateDb.aTables, uses entity metadata helpers, translates definitions to SQL, and sends the SQL through the native wrapper layer.FindRecord,QueryRecords,CreateRecord,UpdateRecord, andDeleteRecordconverttDbMessagepayloads into SQL statements and bind values through the wrapper layer.- Response rows are mapped back into
tDbTableDataso Data Dictionaries see the same record shape they would get from any other driver.
Use Db\cDbFlexAdapter_mixin.pkg
// External_Function declarations for the native SQLite/C module live here.
// Wrap them in small driver methods so adapter operations do not depend on C details.
Class cDbSqliteDriver is a cObject
Procedure Construct_Object
Forward Send Construct_Object
Send Define_cDbFlexAdapter_mixin
End_Procedure
Import_Class_Protocol cDbFlexAdapter_mixin
Function InitializeConnection String sConnectionURI Returns Boolean
// Parse sqlite: URI.
// Call native open wrapper.
// Store native database handle for later adapter operations.
Function_Return True
End_Function
Procedure FindRecord tDbMessage ByRef msg
tDbReqFind req
tDbResFind res
Move msg.request to req
// Build SQL from req.hMainTable, req.aIndex, req.aOrder, and constraints.
// Execute through native wrapper methods.
// Move returned row values into res.aData.
Move res to msg.response
Move C_DbMsgState_Finished to msg.eState
End_Procedure
End_Class
If required operation context such as aStates is missing, mark the message failed and report an error. Do not return an empty success.
HTTP JSON driver example¶
A DDP-style web-service driver is useful when a remote service should look like a Data Dictionary-backed data source. The driver still receives tDbMessage requests, but translates each operation into service calls and maps JSON service responses back into Data Dictionary response structs.
Use Db\cDbFlexAdapter_mixin.pkg
Use System\Http.pkg
Class cDdpServiceDriver is a cObject
Procedure Construct_Object
Forward Send Construct_Object
Send Define_cDbFlexAdapter_mixin
Object oHttp is a cHttpClient
End_Object
End_Procedure
Import_Class_Protocol cDbFlexAdapter_mixin
Function InitializeConnection String sConnectionURI Returns Boolean
Set psEndpoint of oHttp to sConnectionURI
Function_Return True
End_Function
Procedure QueryRecords tDbMessage ByRef msg
tDbReqQuery req
tDbResQuery res
HttpStatus eStatus
Move msg.request to req
// Build a service request from req.hMainTable, req.aFields, req.aOrder, and constraints.
Get HttpPost of oHttp "/query" req to eStatus
If (Err or eStatus >= 400) Begin
Move C_DbMsgState_Failed to msg.eState
Move "Remote query failed." to msg.sError
Procedure_Return
End
// Map the JSON response into res.aData / row IDs.
Get HttpResponseToDataType of oHttp to res
Move res to msg.response
Move C_DbMsgState_Finished to msg.eState
End_Procedure
End_Class
Use the same pattern for create, update, and delete operations by changing the service path and request/response mapping. Keep service payloads driver-private so applications continue using Data Dictionaries and entities.
Custom CSV driver idea¶
This section is a conceptual custom driver example: it shows the kind of adapter an application developer could build when CSV files are useful for import/export, small lookup tables, or demos. The driver would still behave like any other adapter: receive tDbMessage, inspect metadata through msg.hoModel, map rows into tDbTableData, and set msg.eState.
CSV storage can use the sequential file API:
InitializeConnectionparses acsv:URI and stores the file path plus options such as header-row and delimiter handling on the driver object.- Read paths load the file into an in-memory row list. Use the header row to map CSV columns to Data Dictionary fields through
FieldNameandTableDefinition. FindRecordandQueryRecordsfilter the row list withmsg.aStates[0].aConstraints, ordering, row id, and limit/offset constraints, then map rows back intotDbTableDataortDbResQuery.CreateRecord,UpdateRecord, andDeleteRecordrewrite the file after applying the request. This simple pattern is not intended for concurrent multi-user writes.- If the file is missing on read, mark
msgfailed with a driver-facingsError.CreateRecordmay create a missing CSV file and should write the header before the first row.
Use Db\cDbFlexAdapter_mixin.pkg
Use System\FileSystem.pkg
Class cDbCsvDriver is a cObject
Procedure Construct_Object
Forward Send Construct_Object
Send Define_cDbFlexAdapter_mixin
Property String psFilePath
End_Procedure
Import_Class_Protocol cDbFlexAdapter_mixin
Function InitializeConnection String sConnectionURI Returns Boolean
// Parse csv: URI and store the resolved file path/options.
Set psFilePath to sConnectionURI
Function_Return True
End_Function
Procedure FindRecord tDbMessage ByRef msg
tDbReqFind req
tDbResFind res
Move msg.request to req
// Read CSV rows from psFilePath with Seq_New_Channel.
// Direct_Input Channel hFile FILE psFilePath
// Read_Line Channel hFile sLine
// Close_Input Channel hFile
// Send Seq_Release_Channel hFile
// Match req.aIndex plus msg.aStates[0].aConstraints.
// Map the selected CSV row into a tDbTableData item.
Move res to msg.response
Move C_DbMsgState_Finished to msg.eState
End_Procedure
Procedure CreateRecord tDbMessage ByRef msg
tDbReqCreate req
tDbResCreate res
Move msg.request to req
// Append req.vValues to the CSV row set.
// Rewrite the file through Direct_Output / Write_Line.
// Move the created row into res.record.
Move res to msg.response
Move C_DbMsgState_Finished to msg.eState
End_Procedure
End_Class
For custom record separators, place file-name options such as cr: 0: eol: 124: before the path. For production CSV use, quote/escape rules, locking, type conversion, and write atomicity must be driver-owned details; the Data Dictionary-facing contract remains the same tDbMessage request/response shape.
Operation state and metadata¶
Drivers receive these structs from Data Dictionary operations. Application code should keep using Data Dictionaries and should not build adapter messages directly.
| Shape | Fields drivers commonly read or write |
|---|---|
tDbMessage |
eOperation, hoModel, eState, iErrNr, sError, aStates, request, response |
tDbOperationDetails |
aConstraints, aRelations, hMainTable, aTables |
tDbRelation |
hRelatedTable, aRelatedKey, hMainTable, aForeignKey |
tDbConstraint |
eType, hTable, hField, vValue |
tDbTableData |
hTable, rRowId, vData |
tDbFieldValue |
hField, vValue |
CreateMessage initializes eOperation, C_DbMsgState_New, and hoModel. Data Dictionaries populate msg.aStates[0].hMainTable, optional aTables, and operation constraints before EnrichMessage fills the operation plan. EnrichMessage calls DetermineConstraints and NessecaryRelations; drivers should treat msg.aStates[0].aConstraints and msg.aStates[0].aRelations as the already-computed filter/join plan.
FindRecord, QueryRecords, UpdateRecord, and normal DeleteRecord require at least one state item. If SizeOfArray(msg.aStates) < 1, set C_DbMsgState_Failed, fill iErrNr and sError, and return. ZeroFile is the exception because it intentionally deletes the table without state constraints.
Constraint handling:
- Field constraints use
C_DbConstraint_LT,C_DbConstraint_LE,C_DbConstraint_EQ,C_DbConstraint_GE,C_DbConstraint_GT,C_DbConstraint_NE,C_DbConstraint_Matches, andC_DbConstraint_ContainswithhTable,hField, andvValue. C_DbConstraint_RowIdidentifies one record by row id and should be unique in a request.C_DbConstraint_Nativeis a driver-private escape hatch for storage-specific filters. Application code should not depend on it.C_DbConstraint_LimitandC_DbConstraint_Offsetconstrain query paging and should be unique.cDD_Constraints.pkgrejects duplicate unique constraint types for row id, limit, and offset when registering constraint sets.
Current and changed field state:
Field_Current_Valuereads or writes the Data Dictionary buffer without automatically marking a field changed.Field_Changed_Valuewrites and marks changed unless rules such asDD_NoPutor committed-field handling block it.Field_Changed_Statereports or sets whether a field is in the changed-field list.UpdateRecordreceives only changed fields intDbReqUpdate.aData; it does not receive unchanged buffer fields.CreateRecordreceivestDbReqCreate.vValuesfrom the entity buffer. Use metadata such asTableFieldCount,FieldName,FieldType,TableDbName,TableDefinition, andTablePrimaryKeyFieldsto map values.
Handling each message type¶
SaveRecord is a Data Dictionary operation, not a separate adapter message; it dispatches to CreateRecord when HasRecord is false and to UpdateRecord when HasRecord is true.
| Message | Handler | Request | Response | Driver guidance |
|---|---|---|---|---|
C_DbMsg_CreateDb |
CreateDb |
tDbReqCreateDb |
none | Read req.aTables, inspect each table with TableDefinition, create storage if missing or overwrite is requested, and set finished only after all requested tables are handled. |
C_DbMsg_Exists |
CheckTablesExist |
tDbReqExists |
tDbResExists |
Return the subset of req.aTables found in storage as res.aTables; an empty request finishes with an empty response. |
C_DbMsg_Find |
FindRecord |
tDbReqFind |
tDbResFind |
Combine req.aIndex, req.aOrder, req.eMode, req.aParents, and msg.aStates[0].aConstraints; return res.bFound=False plus an empty aData on a clean miss, not an error; when found, put main table data first and parent rows after it as tDbTableData. |
C_DbMsg_Query |
QueryRecords |
tDbReqQuery |
tDbResQuery |
Select requested req.aFields, apply msg.aStates[0].aConstraints, include primary-key row ids in res.aRowIds, return res.aData as row-major Variant[][], and honor C_DbQuery_First, C_DbQuery_Next, C_DbQuery_Prev, and C_DbQuery_Last using req.rStartAt for cursor modes. |
C_DbMsg_Create |
CreateRecord |
tDbReqCreate |
tDbResCreate |
Map req.vValues to storage fields, skip generated/auto-increment fields unless a value is present, write one row, re-read or build the stored row, and return res.record. |
C_DbMsg_Update |
UpdateRecord |
tDbReqUpdate |
tDbResUpdate |
Require req.aData changed fields and constraints, usually including row id; update only listed fields; set res.iAffectedRows; return the refreshed record in res.record when req.bReturnRecord is true or when the driver follows the existing SQLite pattern of always reseeding the buffer after update. |
C_DbMsg_Delete |
DeleteRecord |
tDbReqDelete |
none | If req.bZeroFile=True, clear the table without normal constraints; otherwise apply req.aConstraints plus msg.aStates[0] relations/constraints so constrained views cannot delete filtered-out records. |
Every handler either sets C_DbMsgState_Finished and any required response, or sets C_DbMsgState_Failed, iErrNr, and sError. Do not silently succeed for unsupported operations; keep the adapter mixin's DFERR_PROGRAM "Not supported" behavior when an operation is not implemented. A clean find miss is finished with bFound=False; storage errors, missing state for stateful operations, unsupported query modes, malformed CSV rows, or failed service calls are failed messages.
Error and state handling¶
- On success, assign any response struct to
msg.responseand setmsg.eStatetoC_DbMsgState_Finished. - On failure, set
msg.eStatetoC_DbMsgState_Failed, setmsg.iErrNrwhen available, and setmsg.sErrorto actionable text. - Connection failures should be reported from
InitializeConnectionand should not leave the connection marked active. - Unsupported operations should fail explicitly; no-op success is incorrect because Data Dictionaries rely on message state and returned data.
- If the driver participates in transaction coordination, implement
BeginTransaction,CommitTransaction, andRollbackTransactionconsistently with the operation methods.
Driver checklist¶
- Register a URI scheme with
RegisterDriver. - Parse
psConnectionUriinInitializeConnection. - Implement every
ProcessMessageoperation the application will call. - Use entity metadata helpers for table names, field names, primary keys, relations, and field types.
- Convert constraints and order fields into the storage engine's native query/filter format.
- Map returned rows into
tDbTableData. - Set message state and error fields for every operation path.
- Decide transaction behavior before save/update/delete operations run.