Skip to content

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:

  • psConnectionId
  • psConnectionUri
  • InitializeConnection String sConnectionURI Returns Boolean
  • IsAdapter Returns Boolean
  • ProcessMessage 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_New
  • C_DbMsgState_Pending
  • C_DbMsgState_Failed
  • C_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; response tDbResFind returns bFound plus aData.
  • tDbReqCreate: hMainTable, vValues; response returns one tDbTableData record.
  • tDbReqUpdate: hMainTable, aConstraints, bReturnRecord, aData; response returns affected row count and optional returned record.
  • tDbReqDelete: hMainTable, aConstraints, bZeroFile.
  • tDbReqCreateDb and tDbReqExists: aTables; create also receives bOverwrite.
  • 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_LT
  • C_DbConstraint_LE
  • C_DbConstraint_EQ
  • C_DbConstraint_GE
  • C_DbConstraint_GT
  • C_DbConstraint_NE
  • C_DbConstraint_Matches
  • C_DbConstraint_Contains
  • C_DbConstraint_RowId
  • C_DbConstraint_Native
  • C_DbConstraint_Limit
  • C_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:

  1. InitializeConnection parses the sqlite: URI, calls the native open function through an External_Function wrapper, and stores the returned database handle on the driver object.
  2. CreateDb reads tDbReqCreateDb.aTables, uses entity metadata helpers, translates definitions to SQL, and sends the SQL through the native wrapper layer.
  3. FindRecord, QueryRecords, CreateRecord, UpdateRecord, and DeleteRecord convert tDbMessage payloads into SQL statements and bind values through the wrapper layer.
  4. Response rows are mapped back into tDbTableData so 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:

  1. InitializeConnection parses a csv: URI and stores the file path plus options such as header-row and delimiter handling on the driver object.
  2. Read paths load the file into an in-memory row list. Use the header row to map CSV columns to Data Dictionary fields through FieldName and TableDefinition.
  3. FindRecord and QueryRecords filter the row list with msg.aStates[0].aConstraints, ordering, row id, and limit/offset constraints, then map rows back into tDbTableData or tDbResQuery.
  4. CreateRecord, UpdateRecord, and DeleteRecord rewrite the file after applying the request. This simple pattern is not intended for concurrent multi-user writes.
  5. If the file is missing on read, mark msg failed with a driver-facing sError. CreateRecord may 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, and C_DbConstraint_Contains with hTable, hField, and vValue.
  • C_DbConstraint_RowId identifies one record by row id and should be unique in a request.
  • C_DbConstraint_Native is a driver-private escape hatch for storage-specific filters. Application code should not depend on it.
  • C_DbConstraint_Limit and C_DbConstraint_Offset constrain query paging and should be unique.
  • cDD_Constraints.pkg rejects duplicate unique constraint types for row id, limit, and offset when registering constraint sets.

Current and changed field state:

  • Field_Current_Value reads or writes the Data Dictionary buffer without automatically marking a field changed.
  • Field_Changed_Value writes and marks changed unless rules such as DD_NoPut or committed-field handling block it.
  • Field_Changed_State reports or sets whether a field is in the changed-field list.
  • UpdateRecord receives only changed fields in tDbReqUpdate.aData; it does not receive unchanged buffer fields.
  • CreateRecord receives tDbReqCreate.vValues from the entity buffer. Use metadata such as TableFieldCount, FieldName, FieldType, TableDbName, TableDefinition, and TablePrimaryKeyFields to 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.response and set msg.eState to C_DbMsgState_Finished.
  • On failure, set msg.eState to C_DbMsgState_Failed, set msg.iErrNr when available, and set msg.sError to actionable text.
  • Connection failures should be reported from InitializeConnection and 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, and RollbackTransaction consistently with the operation methods.

Driver checklist

  1. Register a URI scheme with RegisterDriver.
  2. Parse psConnectionUri in InitializeConnection.
  3. Implement every ProcessMessage operation the application will call.
  4. Use entity metadata helpers for table names, field names, primary keys, relations, and field types.
  5. Convert constraints and order fields into the storage engine's native query/filter format.
  6. Map returned rows into tDbTableData.
  7. Set message state and error fields for every operation path.
  8. Decide transaction behavior before save/update/delete operations run.