You are here: Native API > Users Guide > Database Manipulation

Database Manipulation

5.1 Introduction

Using RDM Embedded, databases are manipulated by C language application programs through calls to functions provided in the RDM Embedded runtime library. Database manipulation capabilities include all functions needed to create, retrieve, modify and delete information in a database. Also included are functions that control the operation of the RDM Embedded runtime environment. The library functions that pertain specifically to multi-user database manipulation are described in the RDM Embedded Multi-User Guide.

The RDM Embedded runtime library contains all the functions that make up the core API (the "d_" API), which is the subject of this chapter. RDM Embedded also offers an SQL API, which is described in the RDM Embedded SQL Guide. You can use either or both of these APIs in your program, but in general programs that use the d_ API will be faster than those that use the SQL API. The SQL library calls d_ functions in the runtime library for all database access.

The runtime library functions are grouped into different categories based on each one's purpose. See the list of categories below.

database control Functions that open, close, and initialize databases, and set runtime control parameters and options.
currency control Functions that access and manipulate the currency tables.
data retrieval Functions that access and read information from the database.
data creation All functions used to store new information and relationships in the database.
data modification Functions that modify information and relationships stored in the database.
data deletion Functions used to remove information and relationships from the database.

Note that in the RDM Embedded runtime library all API function names are prefixed by d_ (for example, d_keyfind) so as to avoid name conflicts with other user or system library functions. The return value of all RDM Embedded functions is an integer completion status for the requested operation. A status code of zero (S_OKAY) indicates that the operation completed successfully. These status codes will be introduced as required in the discussions that follow. A complete list can be found in Database Error Reporting, and complete descriptions are provided in the RDM Embedded Reference Manual.

The purpose of this chapter is to introduce the use of the principal functions through explanation and examples. Complete details relating to the use of each function are provided with hyperlinks into the RDM Embedded Reference Manual.

Most of the RDM Embedded functions must be passed a task parameter and a database number. In the examples, a task pointer called task and a constant called CURR_DB ("use the current database") has been passed to the functions. The task pointer must always be passed to RDM Embedded functions to facilitate reentrancy. The database number must always be passed, even when only one database is open. See Accessing Multiple Databases, for complete details regarding multiple database access and the database number argument.

5.2 Database Control

Database control functions provide control over the runtime system's operational environment. They provide for the opening and closing of databases, location and names of database files, initialization of databases and files, and various runtime tuning parameters and options.

The database control functions listed below have been grouped into two categories. The "pre-open" functions are those that can only be called prior to a d_open call, which opens a database. The "post-open" functions can only be called after databases have been opened. Functions d_off_opt and d_on_opt, however, can be executed either before or after databases have been opened.

Pre-Database Open Functions
d_dbuserid Set database user identifier.
d_dbver Obtain runtime library version information.
d_open Open a (set of) database(s).
d_open_ptr Open a database using a dbd array.
d_opentask Create a new task context.
d_setpages Set the number of pages in the cache.
d_dbini Set path to the RDM Embedded initialization file.

Note, as of RDM Embedded Version 10, dbuserid's are automatically assigned. Therefore d_dbuserid is unnecessary, but is left intact for compatibility.

5.2.1 Opening and Closing Databases

Functions d_open, d_iopen, d_open_ptr, and d_iopen_ptr are called to open databases. They are called with the name(s) of the database(s) to be opened. The d_open and d_open_ptr functions have an additional parameter that identifies the mode of database access (see Transaction Processing).

Opening Databases

The following code will open database TIMS in one-user access mode:

status=d_opentask(&task);
status=d_open("tims", "o", task);

The d_opentask function allocates a structure to store all context information normally associated with a task (normally a task is one process or thread). When d_open is called, the RDM Embedded runtime library will allocate and initialize memory space for all of its internal tables, and will read into memory the database dictionary for the requested databases. Memory is also allocated for the RDM Embedded cache. If there is not enough memory available for the tables or cache, d_open will return status code S_NOMEMORY (see function d_setpages).

The d_open function requires that the database's dictionary file (DBD) already exists in the TFS. Installation of the DBD file is accomplished through copying the dbname.dbd file into the TFS environment, in the database's directory, or by rerunning ddlp with the database's DDL file.

It is possible to avoid the above steps by using the d_open_ptr function, which embeds the database dictionary into the program. Thus the ddlp can be run in the development environment to create the program, but the DBD file is no longer necessary. The ddlp option "-c" is used to generate the necessary files. For example:

ddlp -c tims.ddl

Which results in the generation of three source files: tims_dbd.c, tims_dbd.h, and tims.h. The tims.h file is the one used for declaring variables that match record structures, and also contains constants to identify all database elements. The tims_dbd.c and tims_dbd.h files are used for embedding the TIMS dictionary into the program as follows:

#include "rdm.h"
/* include the TIMS dictionary */
#include "tims_dbd.h"
/* include the TIMS structures and constants */
#include "tims.h"

...
main()
{
    ...
    status = d_opentask(&task);
    status = d_open_ptr("tims", "o", &tims_dbd, sizeof(tims_dbd), task);

The tims_dbd.c file must be compiled and linked with the program. The following example assumes Microsoft C.

cl -MD -DWINDOWS_X86 -I..\include tims.c tims_dbd.c ..\win32\lib\rdmerdm10.lib

Database access or update functions called prior to a successful opening of the database will return error code S_DBOPEN.

If a subsequent d_open call is made before the first database is closed, RDM Embedded will abort any active transaction, close the open database(s), and open the new database(s).

RDM Embedded also supports the ability to open multiple databases within the same task, and the ability to incrementally open (open one or more databases within the same task after one or more databases are already open) databases. See Multiple Database Access below for how to use d_iopen and d_iopen_ptr.

Closing Databases

Function d_close will close all open databases. If a transaction was active, it will be aborted.

If multiple databases are open, then d_iclose may be used to close one of them. See Multiple Database Access below for the complete discussion.

5.2.2 Operational Environment

An RDM Embedded database consists of its dictionary (dbname.dbd) , the data and key files (as named in the DDL), and log files used for the purpose of committing transactions, mirroring and replication.

All files associated with a database are stored in the document root of the Transactional File Server (TFS). When a program calls d_open, the RDM Embedded runtime library will connect to the TFS. When the TFS is local (that is, on the same computer), the TFS will be located through a default name and port (localhost on port 21553). If the TFS is running on a different computer, the name and port are taken from the TFS qualifier (part of the database name in the d_open call, see below), or the RDME_TFSERVER environment variable.

Figure 5-1 shows the common same-computer configuration.


Fig. 5-1. Same-computer Configuration

Database files are stored in the TFS document root under a subdirectory named after the database. No database files will be stored or located outside the database's subdirectory.

If the TFS document root is c:\RDMe\databases, then the following directory structure will exist for example databases tims, sales and invntory:

c:\RDMe\databases\
    invntory\
        invntory.d00
        invntory.d01
        invntory.d02
        invntory.dbd
        invntory.k00
        invntory.k01
        invntory.k02
        invntory.v00
    tims\
        tims.d01
        tims.d02
        tims.dbd
        tims.k01
        tims.k02
        tims.v01
        tims.v02
    sales\
        sales.d00
        sales.d01
        sales.dbd
        sales.k00
        sales.k01
        sales.k02
        sales.v00

The TFS determines its document root from the command-line:

c:\RDMe> tfserver -d c:\RDMe\databases

or if no command-line option is specified, the current directory:

c:\RDMe> cd databases
c:\RDMe\databases> tfserver

If the TFS is running on a different computer, the TFS host must be visible to the computer on which the application is running. In a LAN connecting the workstations in an office, the computers are normally visible to each other and have a high-bandwidth connection. If necessary, obtain the computer names from your network administrator.

Wide-area networks (WANs) are viable platforms for RDM Embedded deployment, where the TFS hosts may be identified with domain names that are public on Internet (e.g. www.RaimaPublicData.com:21553). When a TFS is public, it is important, of course, to make sure that no private or vulnerable data is available.

NOTE: The computer or domain name is not the same as a TFS name (the TFS doesn't have a name), but the TFS will be located through the computer's name. You can test your connectivity to the computer with a ping command, for example:

c:\RDMe> ping henry_lptp
c:\RDMe> ping www.RaimaPublicData.com

When an application calls d_open, the socket library function gethostbyname will be used to determine visibility of the TFS host's name.

Since each TFS operates within it's own document root, and each TFS is identified through it's host computer name and port, it is possible to have multiple TFS's running on the same computer, and to connect to multiple TFS's on the same or other computers. Figure 5-2 shows a variety of connectivity options:


Fig. 5-2 Connectivity Options

Multiple databases (see Multiple Database Access) that are located in different TFS's can be opened simply by qualifying the database name with the TFS computer name and port, as in the following example:

d_open("tims;tims@RDMe_svr", "o", task); 

Here, database 0 would be the tims database located on the TFS running on same computer (localhost) with default port 21553, and database 1 would be the tims database located on the TFS running on the computer named RDME_svr with default port 21553. Both database directories would need to contain the dictionary file tims.dbd even if they are identical.

If a local TFS is using a port other than the default port, it will be necessary to qualify the database name with "@localhost:port", which will work with any local TFS regardless of the computer name.

When multiple TFS's are running on the same computer, to avoid a port conflict, at least one must have a non-default port number. For example, on the computer RDMe_svr above, the two TFS's could be started as follows:

c:\RDMe> tfserver -d c:\RDMe\databases
c:\RDMe> tfserver -d c:\RDMe\directories -p 1730

Then, the Application B running on Joe_wkstation could perform the following d_open call:

d_open("tims@RDMe_svr;phonelist@RDMe_svr:1730", "o", task);

Mirroring

NOTE: Mirroring functionality requires either the HA or Flow packages of RDM Embedded. See Packages in RDM Embedded.

When a database is mirrored from another TFS, it is placed into a subdirectory named after the host computer and port. For example, if the local TFS has a document root c:\RDMe\databases containing a database phlist, and a database named phlist is being mirrored from a TFS on the computer named tfs.corporate.com, then the following local files will be found:

c:\RDMe\databases
    phlist\
        phlist.d00
        phlist.dbd
        phlist.k01
        phlist.k02
    tfs.corporate.com-21553\
        phlist\
            phlist.d00
            phlist.dbd
            phlist.k01
            phlist.k02
    tims\
        tims.d01
        ...

Note that any other databases mirrored from the same remote site will be placed in the same subdirectory.

When a database is mirrored, it must be opened read-only on the local computer, so either of the following calls are valid:

/* open two local databases */
d_open("tims;tfs.corporate.com-21553/phlist", "r", task);

/* open locally maintained and mirrored phlist as a union */
d_open("phlist|tfs.corporate.com-21553/phlist, "r", task);

Opening a mirrored database is different than opening a remote database. For example, the following open call is opening both the remote and local phlist databases in shared mode, meaning that updates may be made to either or both of the databases:

/* open the local and remote phlist databases */
d_open("phlist;[email protected]", "s", task);

Environment Variable

The environment variable RDME_TFSERVER is checked when a database is being opened with no qualification on a database name. The value of RDME_TFSERVER may be a computer name, or a computer name followed by a port:

c:\RDMe> set RDME_TFSERVER=henry_lptp

which would use the default port 21553, or:

c:\RDMe> set RDME_TFSERVER=RDMe_svr:1730

to contact the TFS on RDMe_svr with port 1730, which would have been started as follows:

c:\RDMe_svr\RDMe\databases> tfserver -p 1730 

In-Memory Files and Paths

The locations of data, key and vardata files specified through DDL as inmemory are the same as their disk-based counterparts. This way, more than one database and more than one TFS (each with its own document root) can be stored in the same shared memory on one computer host.

5.2.3 Dynamic Database Initialization

Dynamic database initialization functions allow a program to initialize an entire database, or one or more files, in a database at program execution time. These functions are useful when a first-time initialization needs to be included as part of an application or when a temporary database is needed.

These functions will destroy existing data. They are not part of a normal database open sequence.

Function d_initialize will initialize all database data and key files associated with the open database. In multi-user environments, it can only be called when the database is opened in exclusive access mode, or if all of the record types in the database are write-locked. Function d_destroy is called to close the database and to delete all of the database files comprising the open database.

Individual files that have a fileid specified in the DDL file statement can be initialized using function d_initfile. For example, the following DDL defines files that keep track of a daily user login history:

key file day_key = "dayfile.key" contains login_id; 
data file day_data = "dayfile.dat" contains login_history; 

To create a specific user's own login database, ddlp can be run using the namespace option:

c:\RDMe> ddlp -ns wlw login.ddl

resulting in a database named wlwlogin.

Assuming that the following example program receives the user's initials as argv[1], the login database files are initialized by the application when the user logs in at the beginning of the day, as follows:

#include "rdm.h"
#include "login.h"
#include "mis.h"

main(int arc, char *argv[])
{
    DB_TASK *task;
    char myDbs[16];
    ...

    /* Open Login and Mgt Info System databases */
    strcpy(myDbs, argv[1]);
    strcat(myDbs, "login;mis");
    d_opentask(&task);
    d_open(myDbs, "s", task);
    ...

    /* Since each user has his own login database, an
       exclusive lock on that database will preclude the use of
       other locks and yield better performance. */
    d_reclock(LOGIN_HISTORY, "x", task, CURR_DB);

    if (beginning_of_day) {
        /* Initialize daily login files */
        d_initfile(DAY_KEY, task, CURR_DB);
        d_initfile(DAY_DATA, task, CURR_DB);
    }
    ...
}

5.2.4 Runtime Control

Runtime control functions inform the RDM Embedded runtime of the size of the database cache, or to turn on or off one or more of several runtime options. These functions provide the flexibility to tune runtime performance to meet the requirements of individual applications.

Size of Runtime Cache

RDM Embedded performs all input and output for the database files through a cache consisting of a fixed number of database page buffers. This technique yields large performance benefits by reducing the number of actual disk accesses required to read or write information in the database.

The programmer can specify the number of buffers to allocate for the cache with function d_setpages. In general, the more pages specified the better the potential performance gains. The first argument to this function specifies the number of pages in the standard database cache. The second argument is no longer used and exists to maintain backward compatibility.

If d_setpages is not called, RDM Embedded will allocate 100 pages in the database cache. The cache lookups are performed using a hashing algorithm, as shown below:

d_setpages(200, 0, task); 

The initial size of the pages allocated for the database cache will be equal to the size of the largest page in the database. Thus, if the largest page is 4096 bytes long, then the database cache in the above example will occupy 800K bytes (200 * 4096). If there is not enough memory available to accommodate the requested number of pages, function d_open will return status S_NOMEMORY.

Option Settings

Various runtime option settings allow you to do these activities plus others:

Rather than supplying separate functions to control the setting of these runtime options, RDM Embedded provides two parameter-based system option setting functions called d_on_opt and d_off_opt. These functions are passed a bit status word, which has a bit associated with each option. These options have been assigned constants in rdm.h. See the RDM Embedded Reference Manual for a complete list.

Here are some examples. To turn on case-insensitive sorting:

d_on_opt(IGNORECASE, task); 

This option will re-define the collating sequence of characters, and must be used for the lifetime of a database. You cannot build a database with this option turned on, then later use it with it turned off.

To turn off delete chain use and stop ignoring environment variables:

d_off_opt(DCHAINUSE | IGNOREENV, task); 

The ability to turn on or off the use of deleted record slots provides some application control over the placement of related records in the database. If all member record occurrences of a given set are entered together, and use of the delete chain is turned off, the records will all be physically placed in the order in which they're entered at the end of the data file. This will improve performance when, later, they are all accessed together.

The default settings have delete chain use turned on, and case-insensitive sorting turned off.

5.3 Currency Tables

All of the data contained in an RDM Embedded database is navigated through use of the currency tables. Thus, a thorough understanding of the use of these tables is necessary.

The retrieval of RDM Embedded data is always a two-step process. First the location of the data is established, and then the data is read. The located data is always in the form of a specific record occurrence. The database address of a record is the location in the database where the record is stored. Normally, a program will not need to obtain the actual database address, but can use a current database address. There will be current database address for the most recently visited record, called the current record, and for the most recently visited set owner or set member (one owner/member pair for each set type). Together, these are called the currency tables. They can be thought of as multiple bookmarks in the database.

A currency table value is established through the record location functions (for example, d_keyfind or d_findnm), and through additional functions that directly modify currency table entries (for example, d_setor copies the current record value to the current owner entry of the specified set). Once a record has been located, its database address is automatically stored in the currency table. Its contents can then be read (for example, d_recread reads the contents of the current record).

The following code fragment shows how currency tables are implicitly used by many of the core functions. It answers the question from the TIMS database "show me all articles published by the author who published the book The Network-Model DBMS" (the id code for this book is "db021").

#include "tims.h"
...

struct info infoRec;
int32_t rc;

d_open("tims", "x", task);

/* first task, find the article */
d_keyfind(ID_CODE, "db021", task, 0);
/* now this INFO record "db021" is current record */

/* set the current member of HAS_PUBLISHED from the current record */
d_setmr(HAS_PUBLISHED, task, 0);
/* now current owner of HAS_PUBLISHED is AUTHOR "Kruglinshi, D." */
/* now current member of HAS_PUBLISHED is INFO "db021" */

rc = d_findfm(HAS_PUBLISHED, task, 0);
/* now the current record is the first member of HAS_PUBLISHED */
/* now the current member of HAS_PUBLISHED is the first member */

/* scan through the HAS_PUBLISHED set for this AUTHOR */
while (rc == S_OKAY) {
    /* read the current record's data into a local variable */
    d_recread(&infoRec, task, 0);
    printf("Title: %s\n", infoRec.info_title);
 
    /* move to the next member, if there is one */
    rc = d_findnm(HAS_PUBLISHED, task, 0);
    /* now the current record is the next member of HAS_PUBLISHED */
    /* now the current member of HAS_PUBLISHED is the next member */
}

Figure 5-3 below shows what the currency would represent after the first call to d_findnm above. Note that the loop will execute d_findnm a second time, but there is not another member in the set. This will cause the current member of HAS_PUBLISHED to be set to NULL_DBA, and S_EOS will be returned from the function. This will cause the loop to terminate correctly.


Fig. 5-3. Currency State

Because of the importance of the currency tables, a rich set of functions is provided to give the programmer complete control over currency table settings. These functions are listed below. The d_setXY functions have a naming convention in which X specifies the destination currency and Y specifies the source currency for the assignment operation. For example, in d_setro, the 'r' indicates that the current record is to be assigned and the 'o' indicates that it is to be assigned from the current owner of the specified set.

Currency Manipulation Functions
d_crset Set database address of current record from variable.
d_csmset Set database address of current member of SET from variable.
d_csoset Set database address of current owner of SET from variable.
d_setro Set current record from current owner of SET.
d_setrm Set current record from current member of SET.
d_setor Set current owner of SET from current record.
d_setom Set current owner of SET1 from current member of SET2.
d_setoo Set current owner of SET1 from current owner of SET2.
d_setmr Set current member of SET from current record.
d_setmo Set current member of SET1 from current owner of SET2.
d_setmm Set current member of SET1 from current member of SET2.
Currency Access Functions
d_crget Get database address of current record.
d_csmget Get database address of current member of SET.
d_csoget Get database address of current owner of SET.
Currency Table Functions
d_rdcurr Read the currency table into buffer.
d_rerdcurr Reread the currency table into existing buffer.
d_wrcurr Write the currency table from buffer and free buffer.

5.4 Data Retrieval

Table 5-4 lists the functions used for data retrieval.

Table 5-4 Data Retrieval Functions
Key Access Functions
d_keyexist Determine if optional key has been stored.
d_keyfrst Find record with first KEY.
d_keylast Find record with last KEY.
d_keyfind Find first record with exactly matching KEY value.
d_keynext Find record with next KEY.
d_keyprev Find record with previous KEY.
d_keyread Read contents of last key scanned.
d_curkey Set current keys from current record.
Set Navigation Functions
d_findfm Find first member of SET.
d_findlm Find last member of SET.
d_findnm Find next member of SET.
d_findpm Find previous member of SET.
d_findco Find owner of current record.
d_ismember Determine set membership.
d_isowner Determine set ownership.
d_members Obtain a count of the members in this set instance.
Direct Access Retrieval Functions
d_recfrst Find first occurrence of record type REC.
d_reclast Find last occurrence of record type REC.
d_recnext Find next occurrence of current record type.
d_recprev Find previous occurrence of current record .
d_csmread Read data from field of current member.
d_csoread Read data from field of current owner.
d_encode_dba Encode database address from file and slot number.
d_decode_dba Decode file and slot number from database address.

Record occurrences are located using RDM Embedded's key retrieval functions, set navigation functions, direct access, or any combination of these.

5.4.1 Data Retrieval Using Keys

Key field usage from the database design perspective was introduced in Logical Design Considerations. The database manipulation aspects of key field usage is presented here, using the C code that would implement the examples presented in that section.

Code Validation

The following example from the "vehicle make" example illustrates how to use keys to validate coded data fields.

char vma_desc[25]; 	       /* variable to contain vehicle make */
struct fleet f; 		/* variable to hold a fleet record */

get_user_info(&f); 		/* fleet record entered by user */

/* validate correct vehicle make code */
if (d_keyfind(VMA_CODE, f.vma, task, CURR_DB) == S_NOTFOUND)
    entry_error("invalid vma code");
else
{
    /* read vehicle description */
    d_crread(VMA_DESC, vma_desc, task, CURR_DB);

    /* enter fleet record */
    ...
}

The vehicle make code entered by the user as part of the fleet record is used as the key value argument of the d_keyfind function, to check that a vehicle record for that make exists in the database. If the record exists, its description is read (function d_crread) and is displayed to the user once the record entry is completed. VMA_CODE and VMA_DESC are constants defined in the database header file created by ddlp. The second argument to both d_keyfind and d_crread are pointers to the variables that contain the necessary data.

Retrieval Based On a Range of Values

Recall from the "checking account" database example of Database Design, that the check record type contained a key field called check_date. This field was defined as a key field to facilitate rapid retrieval of the check record occurrences for a particular time period. The example code below prints the checks dated in the month of May, 1993.

#include "rdm.h"
#include "ckngacct.h"

struct check chk;     /* check record variable */
int start_date;       /* julian of start date of range */
int end_date;         /* julian of end date of range */
int date;             /* last scanned check date */
DB_TASK *task;        /* default task */

/* application functions found elsewhere */
extern char *calendar();     /* converts julian to calendar date */
extern int julian();         /* converts calendar to julian date */

main()
{
    int status;

    status = d_opentask(&task);
    status = d_open("ckngacct", "o", task);

    start_date = julian("05/01/93");
    end_date = julian("05/31/93");

    /* position to first key in range */
    if ((status=d_keyfind(CHECK_DATE, &start_date, task, CURR_DB)) == S_NOTFOUND)
        status = d_keynext(CHECK_DATE, task, CURR_DB);


    /* scan thru all keys in range */
    while (status == S_OKAY)     {
        d_keyread(&date, task);
        if (date > end_date)
            break;         /* no longer in range */

        d_recread(&chk, task, CURR_DB);
        printf("number   : %d\n", chk.check_no);
        printf("date     : %s\n", calendar(chk.check_date));
        printf("paid to  : %s\n", chk.paid_to);
        printf("amount   : $ %f.2\n", chk.amount);
        status = d_keynext(CHECK_DATE, task, CURR_DB);
    }
    d_close(task);
    d_closetask(task);
}               

The d_keyfind call will position to the first check date key equal to start_date. If there are no checks dated 05/01/93 on file, then function d_keynext is called to position to the first check whose date is greater than the start date. The while loop first reads the value of the positioned key using function d_keyread. Note that this reads the key value only and does not read the record. The record contents are only read if the check date returned from the d_keyread call is within the desired range. Function d_recread reads the contents of the current record for display by the subsequent printf calls. The final d_keynext call positions to the next check date key.

Complex Searches

The m.o. (modus operandi) example of Database Design, introduced how complex searches can be rapidly performed through the use of keys. The m.o. record consists of a single 25-byte key field in which each element of the array represents a coded m.o. attribute.

The simplest approach is to scan through all of the mo_data keys, checking each one for a match, as follows:

char mo_key[25]; 		       /* m.o. key is 25 byte array */
char mo_search[25]; 			/* user entered search data */
int status;

... 					/* user enters m.o. search data */

for (status = d_keyfrst(MO_DATA, task, CURR_DB);
    status == S_OKAY;
    status = d_keynext(MO_DATA, task, CURR_DB) )
{
    d_keyread(mo_key, task);
    if (mo_match(mo_key, mo_search))
        ... 				/* report match */
}              

Function mo_match checks the m.o. key against the m.o. search data entered by the user, returning true (that is, non-zero) if they match and false (that is, zero) if they do not. A zero value in an m.o. search data element means that attribute is not to be used in the search.

/* Check for matching m.o.s */
mo_match(char *mo1, char *mo2)
{
    int x;
    for ( x = 0; x < 25; ++x )
    {
        if ((mo2[x] != 0) && (mo1[x] != mo2[x]))
            return (0);
    }
    return (1);
}

Since keys are sorted, the number of keys that need to be scanned can be reduced when the first element (and any subsequent elements) is non-zero. For example, if the user has supplied m.o. values for the first three elements, a key scan of only the keys on file with those values can be performed, yielding great performance improvements.

char mo_key[25];        /* m.o. key is 25 byte array */
char mo_search[25];     /* user entered search data */
int32_t mo_prefix;      /* initial non-0 elements in mo_search */
int32_t lc;             /* loop control index */
int32_t status;

    ...                 /* user enters m.o. search data */

/* compute mo_prefix */
for (mo_prefix = 0; mo_search[mo_prefix] != 0; ++mo_prefix)
    ;         /* count number of initial non-0 elements */

/* initialize mo_key */
for (lc = 0; lc < 25; ++lc)
{
    if ( lc < mo_prefix )
        mo_key[lc] = mo_search[lc];
    else
        mo_key[lc] = 0;
}

/* position to first key with matching prefix */
if ((status=d_keyfind(MO_DATA, mo_key, task, CURR_DB)) == S_NOTFOUND)
    status = d_keynext(MO_DATA, task, CURR_DB);

/* scan all keys with matching prefix */
while (status == S_OKAY)
{
    d_keyread(mo_key, task);

    /* ensure prefix still matches */
    for (lc = 0; (lc < mo_prefix) && (mo_key[lc] == mo_search[lc]); ++lc)
        ;
    if (lc < mo_prefix)
        break;         /* prefix doesn't match - scan ends here */
    if (mo_match(mo_key, mo_search))
        ...             /* report match */
    status = d_keynext(MO_DATA, task, CURR_DB);
}

Notice that this example is very similar to the example of retrieval by a range of values. Also notice that if the first element of mo_search is zero, all mo_data keys will be checked.

Using Compound Keys

Suppose that the tims borrower record definition (presented in Database Design) were modified such that the friend's name was not one field, but two (for the last and first name). A compound key may be defined to make one key from the two fields:

record borrower
{
    key char fr_last[16];
    char fr_first[16];
    long date_borrowed;
    long date_returned;
    compound unique key friend
    {
        fr_last;
        fr_first;
    }
}

Note that the last name may be used as a key by itself. A new key definition, named friend, is defined in the record. (It must also be included in a key file list.)

When ddlp encounters a compound key, it creates a special structure for the key in the header file it generates. In this case, within tims.h will be the following structure definition:

struct friend { 
    char  fr_last[16];
    char  fr_first[16]; 
}; 

The definition is intended to be used to perform searches for compound keys, as in the following code fragment:

#include "tims.h" 
... 
struct friend fr; 
... 
printf("Last name:  "); 
gets(fr.fr_last); 
printf("First name: "); 
gets(fr.fr_first); 
if (d_keyfind(FRIEND, &fr, task, CURR_DB) == S_NOTFOUND)
    printf( "No one by that name found\n" ); 
else {
    ...   /* use the borrower record */ 
} 

Using Optional Keys

Key fields may be declared optional in the DDL. When a record containing optional key(s) is created, the optional key(s) is (are) not stored in a key index.

To include a record's optional key in the key file, the d_keystore function must be called while the record is the current record. An optional key may be deleted by using d_keydel. If an optional key has been stored, then the function d_keyexist will return S_OKAY. It will return the S_NOTFOUND status otherwise.

Optional keys may be used to defer key creation, or to permit key scanning or searching of a subset of a record type. For example, bulk record creation may occur during busy times, but an after-hours process may scan through the records and create the keys that don't yet exist.

5.4.2 Data Retrieval Using Sets

The process of retrieving records from a database by moving through the various set relationships defined in the schema is called set navigation. A general procedure for navigating sets follows:

  1. Find the record that is the owner of the set whose members are to be read. Typically, this can be done using keys, by iteratively applying this procedure, or by a combination of both.
  2. Make the located owner record the current owner of the set to be traversed, using an appropriate currency manipulation function (such as d_setor).
  3. Find the members of the set, using the set navigation functions (for example, d_findnm will set the current record and current member of the set to the next member record).

Each of the set member find functions (d_findXm) will set both the current member and the current record to point to the found record occurrence. A reciprocal function, d_findco, will set the current owner of the specified set from a current record that is connected through the specified set. The currency changes made by each RDM Embedded function are identified in the function descriptions in the RDM Embedded Reference Manual.

As an example of the above procedure, consider the transactions set from the checking account example in the Database Design. The budget record is the owner and the check record is the member. The following code will display all of the checks written against budget category "FOOD".

/* locate the budget record for the FOOD budget */ 
d_keyfind(CODE, "FOOD", task, CURR_DB);
 
/* make the FOOD budget the current owner of set transactions */ 
d_setor(TRANSACTIONS, task, CURR_DB);

/* find each member of the set and read and print its contents */ 
for (status = d_findfm(TRANSACTIONS, task, CURR_DB);  
    status == S_OKAY; 
    status = d_findnm(TRANSACTIONS, task, CURR_DB)) 
{ 
    d_recread(&chk, task, CURR_DB); 
    ... /* print check record */ 
} 

The set navigation procedure above describes a top-down navigation, wherein the owner is located and then the members. RDM Embedded also provides the ability to first locate a member and then the owner by using function d_findco, which finds the owner of the current record for the specified set. For example, the following code will locate a check record by check number, and then find and print its budget category.

struct check chk;   /* check record variable */ 
struct budget bud;  /* budget record variable */ 

/* locate check numbered 3104 */ 
chk.check_no = 3104; 
d_keyfind(CHECK_NO, &chk.check_no, task, CURR_DB); 

/* read check record contents */ 
d_recread(&chk, task, CURR_DB); 

/* find its owner thru the transactions set */ 
d_findco(TRANSACTIONS, task, CURR_DB); 

/* read budget record contents */ 
d_recread(&bud, task, CURR_DB); /* print results */ 
printf("check    : %d\n", chk.check_no); 
printf("date     : %s\n", calendar(chk.check_date)); 
printf("paid to  : %s\n", chk.paid_to); 
printf("amount   : $ %f.2\n", chk.amount); 
printf("budget   : %s\n", bud.code); 

Many-to-Many Navigation

The navigation of the students to classes, many-to-many, set example in Database Design, is shown in the following code, which lists the students registered in class "CS101".

struct class crec;     /* class record variable */ 
struct student srec;   /* student record variable */ 

/* find CS101 class record */ 
d_keyfind(CLASS_ID, "CS101", task, CURR_DB); 

/* read contents of class record */ 
d_recread(&crec, task, CURR_DB); 

/* make the class record owner of the my_students set */ 
d_setor(MY_STUDENTS, task, CURR_DB); 

/* scan each member of my_students set */ 
while (d_findnm(MY_STUDENTS, task, CURR_DB) == S_OKAY)  
{ 
    /* find student record which owns current intersect record */ 
    d_findco(MY_CLASSES, task, CURR_DB); 
       
    /* read and print contents of student record */ 
    d_recread(&srec, task, CURR_DB); 
    printf("CS101: %s\n", srec.name); 
} 

Note that function d_setor actually sets both the current owner and the current member of set my_students. The current owner is set to the "CS101" record, and the current member is set to NULL_DBA. This allows the initial call to d_findnm to return the first member of the set. The output from execution of the above code might be:

CS101: Carlson 
CS101: Jones 
CS101: Kelly 
CS101: Smith 

From this example you should be able to produce the code for listing the classes in which a particular student is registered.

Variable-length Text Retrieval

Logical Design Considerations, included a description of the use of multiple-member sets in implementing text data. The code needed to retrieve the text data in this example is quite simple, as shown below. Here the customer note for customer id IBMFL is displayed.

struct customer cust;   /* customer record variable */ 
char text[80];          /* text data - size of largest line */

/* find customer with id IBMFL */ 
d_keyfind(CUST_ID, "IBMFL", task, CURR_DB); 

/* make customer current owner of cust_notes set */ 
d_setor(CUST_NOTES, task, CURR_DB); 

/* fetch each member of set and display text */ 
while (d_findnm(CUST_NOTES, task, CURR_DB) == S_OKAY)  
{
    d_recread(text, task, CURR_DB); 
    printf("%s\n", text); 
} 

Each member of the cust_notes set is an occurrence of either the text30, text55, or text80 record types. Here it does not matter which record type is read, since each contains only one string field. The character array text is used to store the contents of all. The function d_crtype, however, could have been used to determine the type of the text records.

Database Examples from TIMS

One of the requirements for the TIMS database examples was to list technical publications in the library by key word or phrase. This involves traversing the many-to-many relationship between the key_word record and the info record twice (once to get the info record from the specified key word, and again to retrieve all of the key words associated with that located info record), and then scanning the abstract set to retrieve the text for the abstract.

Function by_key lists all of the info records that have the user-entered key word or phrase. For each info record selected, all of its key words and its abstract are printed.

#include <stdio.h> 
#include "rdm.h" 
#include "tims.h" 

/* Find publications by key word */
int32_t by_key(DB_TASK *task)
{
    int32_t status;
    struct info irec;                /* info record variable */
    char name[SIZEOF_NAME];          /* author's name */
    char key[SIZEOF_KWORD];          /* key word */
    char cmd[10];


    /* find key word record */
    printf("key word: ");
    getstring(key,SIZEOF_KWORD);
    if (d_keyfind(KWORD, key, task, CURR_DB) == S_NOTFOUND)
        printf("no records found\n");
    else
    {
        /* scan thru key_to_info set */
        d_setor(KEY_TO_INFO, task, CURR_DB);
        for (status = d_findfm(KEY_TO_INFO, task, CURR_DB); status == S_OKAY;
             status = d_findnm(KEY_TO_INFO, task, CURR_DB))
        {
            /* find current owner (info) of current record (intersect) */
            d_findco(INFO_TO_KEY, task, CURR_DB);

            /* read contents of info record */
            d_recread(&irec, task, CURR_DB);

            /* find author of info record */
            d_findco(HAS_PUBLISHED, task, CURR_DB);
            d_crread(NAME, name, task, CURR_DB);

            /* print results */
            printf("id_code: %s\n", irec.id_code);
            printf("author : %s\n", name);
            printf("title  : %s\n", irec.info_title);
            printf("publ.  : %s, %s\n", irec.publisher, irec.pub_date);
            pr_keywords(task);
            pr_abstract(task);
            printf("--- press <enter> to continue");
            getstring(cmd,sizeof(cmd));
        }
    }
    return (0);
}      

Figure 5-4 illustrates the operation of function by_key up to the point where function pr_keywords is called. The d_keyfind locates the key_word record occurrence, which is then made the current owner of set key_to_info. As each intersect record that is a member of the info_to_key set is found (usingi d_findnm(KEY_TO_INFO, ... )), its owner through the info_to_key set is found (using d_findco(INFO_TO_KEY, ... )), and the info record contents are read. The author is found (and made current) through the has_published set (using d_findco(HAS_PUBLISHED, ... )) and the name is read using function d_crread to read a field of the current record.


Fig. 5-4. Function by_key Operation

Function pr_keywords is called to print all of the key words and phrases associated with an info record (note that at least one key word will appear in each list, the one chosen in the call to by_key). Function d_members returns, in variable count, the number of members in set info_to_key. If there are any members, then the owner of each intersect member record is found through the key_to_info set, and from this owner record the key word is read and displayed. This function (pr_keywords) will change the currency associated with set key_to_info, which is used by function by_key in its scanning of that set. Thus, it is necessary for the current member of key_to_info to be saved (by d_csmget ) before the key words are retrieved, and then restored (by d_csmset, which sets both current member and current owner) after they have been retrieved.

/* Print key words */
static void pr_keywords(DB_TASK *task)
{
    int32_t count;                      /* number of info_to_key members */
    char key[SIZEOF_KWORD];          /* key word or phrase */
    DB_ADDR dba;                     /* db addr of key_to_info member */

    /* the current member of the has_published set is the info record whose
     * key words are to be listed */
    d_setom(INFO_TO_KEY, HAS_PUBLISHED, task, CURR_DB);

    /* fetch number of members of info_to_key */
    d_members(INFO_TO_KEY, &count, task, CURR_DB);

    /* list the key words, if any */
    if (count > 0L)
    {
        /* save current member of key_to_info because it's going to change and
         * we may be currently scanning through that set */
        d_csmget(KEY_TO_INFO, &dba, task, CURR_DB);

        printf("key words:\n----------\n");
        /* find each intersect member record */
        while (d_findnm(INFO_TO_KEY, task, CURR_DB) == S_OKAY)
        {
            /* find, read and print corresponding key_word */
            d_findco(KEY_TO_INFO, task, CURR_DB);
            d_crread(KWORD, key, task, CURR_DB);
            printf("   %s\n", key);
        }
        printf("\n");

        /* reset key_to_info current member and owner */
        if (dba)
            d_csmset(KEY_TO_INFO, &dba, task, CURR_DB);
    }
}

The abstract is printed by function pr_abstract, which simply scans through the abstract set owned by the current info record to read and display each line of abstract text.

/* Print abstract */
static void pr_abstract(DB_TASK *task)
{
    int32_t count;                      /* number of abstract members */
    char txt[80];                    /* line of abstract text */

    /* the current member of has_published is the info record whose abstract
     * is to be printed */
    d_setom(ABSTRACT, HAS_PUBLISHED, task, CURR_DB);

    /* fetch number of lines in abstract */
    d_members(ABSTRACT, &count, task, CURR_DB);

    /* print abstract, if one exists */
    if (count > 0)
    {
        printf("abstract:\n---------\n");

        /* find, read and print each abstract text line */
        while (d_findnm(ABSTRACT, task, CURR_DB) != S_EOS)
        {
            d_csmread(ABSTRACT, LINE, txt, task, CURR_DB);
            printf("  %s\n", txt);
        }
    }
    printf("\n");
}      

The other retrieval requirement is to be able to list all of the publications in the TIMS database that are by a particular author. This is accomplished with function by_author. The author is located by scanning through the (system-owned) author_list set, and comparing a user-specified name with the author name. When a match is found, each info record owned by the located author is read and displayed, along with its associated key words and abstract.

/* Find publication by author
*/
int32_t by_author(DB_TASK *task)
{
    int32_t status;
    size_t searchLen;
    struct info irec;                /* info record variable */
    char search[SIZEOF_NAME];        /* author to search for */
    char name[SIZEOF_NAME];
    char cmd[10];

    /* find author record */
    printf("author: ");
    getstring(search,SIZEOF_NAME);
    searchLen = strlen(search);
    for (status = d_findfm(AUTHOR_LIST, task, CURR_DB); status == S_OKAY;
         status = d_findnm(AUTHOR_LIST, task, CURR_DB))
    {
        d_crread(NAME, name, task, CURR_DB);
        if (strncmp(search, name, searchLen) == 0)
        {
            d_setor(HAS_PUBLISHED, task, CURR_DB);
            for (status = d_findfm(HAS_PUBLISHED, task, CURR_DB);
                 status == S_OKAY;
                 status = d_findnm(HAS_PUBLISHED, task, CURR_DB)) /*lint !e445 */
            {
                d_recread(&irec, task, CURR_DB);

                /* read and print info record */
                printf("id_code: %s\n", irec.id_code);
                printf("author : %s\n", name);
                printf("title  : %s\n", irec.info_title);
                printf("publ.  : %s, %s\n", irec.publisher, irec.pub_date);
                pr_keywords(task);
                pr_abstract(task);
                printf("--- press <enter> to continue");
                getstring(cmd,sizeof(cmd));
            }
        }
        else if (strcmp(search, name) < 0)
        {
            printf("author record not found\n");
            return (0);
        }
    }
    return (0); /*lint !e850 */
}

5.4.3 Direct Access Retrieval

Direct access retrieval is used to locate sequentially all occurrences of a particular record type and to directly read the contents of a record whose database address is known (for example, if it was located earlier through a key, set, or sequential retrieval). Sequential retrieval is performed using these functions:

d_recfrst Locates the first occurrence on the data file of the specified record type.
d_reclast Locates the last occurrence on the data file of the specified record type.
d_recnext Finds the next occurrence of a record of the same type.
d_recprev Finds the next previous occurrence of a record of the same type.
d_recread Read the data in the current record.
d_recset Set the current of record type for scanning position.

When the retrieval order is not important (the physical order of records on a file may seem random), the sequential functions are the quickest way to scan records. The id_list function is an example of such a scan.

/* produce a quick sequential listing of all id_code's */ 
id_list(DB_TASK *task) 
{ 
   int status; 
   char id_code[SIZEOF_ID_CODE]; 

   for (status = d_recfrst(INFO, task, CURR_DB); 
        status == S_OKAY;  
        status = d_recnext(task, CURR_DB))  
   { 
      d_crread( ID_CODE, id_code, task, CURR_DB); 
      printf("%s\n", id_code); 
   } 
} 

These functions maintain their own currency by record type. In a loop, these four functions will maintain their own positional information, even if there are other functions in the loop that change currency table settings. To establish or restore a position from which to continue scanning, the function d_recset can be used. This function will set the current position of a sequential scan from the current record. Thus if a d_recfrst/d_recnext loop needs to contain an inner loop for a different record type, the position can be saved and restored around the inner loop, as follows:

DB_ADDR t1dba; 
int status1, status2; 

for (status1 = d_recfrst(TYPE1, task, CURR_DB);  
     status1 == S_OKAY;  
     status1 = d_recnext(task, CURR_DB))  
{ 
   d_crget(&t1dba, task, CURR_DB); 
   ... 
   for (status2 = d_reclast(TYPE2, task, CURR_DB); 
        status2 == S_OKAY; 
        status2 = d_recprev(task, CURR_DB))  
   { 
      ... 
   } 
   d_crset(&t1dba, task, CURR_DB); 
   d_recset(TYPE1, task, CURR_DB); 
} 

The function page_full begins with the current record (which is assumed to be a key_word type), and creates a list of the next twenty key word occurrences, as shown below:

/* create a list of key words for display */ 
page_full( char **pglist, int dir, DB_TASK *task ) 
 { 
   int i, status; 

   for (status = d_recset(KEY_WORD, task, CURR_DB), i = 0;  
        (status == S_OKAY) && (i < 20); 
        status = (dir ?   d_recnext(task, CURR_DB) : 
                          d_recprev(task, CURR_DB)), i++ )  
   { 
      d_crread(KWORD, pglist[i], task, CURR_DB); 
   } 
} 

Functions d_recread, d_crread, d_csoread, and d_csmread read part or all of the contents of the current record, owner, or member, copying the data to an application program defined buffer. A pointer to this buffer is an argument to each of these functions.

Functions d_decode_dba and d_encode_dba are used to decode and encode a database address with its file number and slot number. They can be used in conjunction with functions d_crget and d_crset to utilize a record's database address as a primary key, which can be displayed and referenced by a user in order to directly access individual record occurrences. For example, a database address for the record at slot number 17112 on file number 11 could be displayed as a primary key field called id number that, to the user, could look like the following:

id number:  11-17112 

By always displaying this number (actually the database address) when a record is displayed, and by requiring this field to be entered whenever the record is modified, the record can be located in a single disk read. The code to display the field follows.

DB_ADDR dba;         /* database address of current record */ 
short file;          /* file number */ 
F_ADDR8 slot;         /* slot number */ 
... 
/* record to be displayed is current record */ 
d_crget(&dba, task, CURR_DB); 
d_decode_dba(dba, &file, &slot); 
printf("id number: %d-%ld\n", file, slot); 

When the user has entered an id number, the record can be read as follows:

DB_ADDR dba;         /* database address */ 
int file;            /* file number */ 
F_ADDR8 slot;        /* slot number */ 
struct record rec;   /* rdm record buffer */ 
... 
/* extract file number of slot number from id number string */ 
... (you supply the tedious stuff) 

/* form database address */ 
d_encode_dba(file, slot, &dba); 

/* set current record and read record contents */ 
d_crset(&dba, task, CURR_DB); 
d_recread(&rec, task, CURR_DB); 

/* display record */ 
... 

5.5 Data Creation

The RDM Embedded functions used to create record and key occurrences and to make set connections are listed in Table 5-5 below.

Table 5-5 Data Creation Functions
Record/Key
Creation Functions
Description
d_fillnew Create and fill contents of new record occurrence.
d_setkey Set key field value for new record.
d_makenew Make a new record occurrence slot and store associated keys.
d_crwrite Write data to specified field of new record occurrence.
d_keystore Create optional key entry.
Set Creation
Function
Description
d_connect Connect new record to SET.

New record occurrences are entered into the database by using either the d_fillnew or d_makenew function. To d_fillnew is passed the record type of the record to be created and a pointer to the record's contents. This pointer usually points to a variable of that record's struct type, as declared in file dbname.h. Function d_makenew creates an (almost) empty record occurrence, in which the field values will be stored later, usually through individual calls to d_crwrite. However, d_makenew must store the key fields in the record and create the key file entries at the time of the call. Prior to the call to d_makenew, function d_setkey must be called for each (non-optional) key field, to save the values of each key field for d_makenew. Generally, it is simplest to use only d_fillnew, which automatically creates the key entries for you without a call to d_setkey. Function d_makenew is useful when a record is to be created before the contents of the fields are known, or for creation of records that have no fields (such as an empty intersection record of a many-to-many set).

Function d_keystore is used to create the key file entries for optional keys in the current record. Optional keys are often used to defer key creation until non-peak system load times, in order to maximize data entry performance.

The data creation process involves not only record creation, but set creation as well. After a record has been created, it often needs to be connected to appropriate sets. This may involve data retrieval to set up the currency tables properly.

Entry of an info record into the TIMS database provides a good illustration of what is typically involved in creating RDM Embedded data.

Three functions are presented below. Function ent_info enters the info record and, if necessary, the author record, and makes the proper set connections. Function enter_key_words is called by enter_info to read each key word from the user and set up the many-to-many relationship with the entered info record. Function enter_abstract is called to read from the user each line of the abstract and connect it to the info record through set abstract. One function that is called but not shown is function get_info, which reads from the user, the author name, and info record fields.

/* Enter technical information records into TIMS database */
int32_t ent_info(DB_TASK *task)
{
    int32_t status;
    char s[SIZEOF_NAME];  /* generic string variable */

    /* enter tech info into TIMS database */
    while (get_info() != EOF)
    {
        /* see if author exists */
        for (status = d_findfm(AUTHOR_LIST, task, CURR_DB); status == S_OKAY;
             status = d_findnm(AUTHOR_LIST, task, CURR_DB))
        {
            d_crread(NAME, s, task, CURR_DB);
            if (strcmp(arec.name, s) == 0)
                break;                        /* author record on file */
        }
        if (status == S_EOS)
        {
            /* author not on file -- create record and connect to author list */
            d_fillnew(AUTHOR, &arec, task, CURR_DB);
            d_connect(AUTHOR_LIST, task, CURR_DB);
        }
        /* make author current owner of has_published set */
        d_setor(HAS_PUBLISHED, task, CURR_DB);

        /* create new tech. info record */
        if (d_fillnew(INFO, &irec, task, CURR_DB) == S_DUPLICATE)
            printf("duplicate id_code: %s\n", irec.id_code);
        else
        {
            /* connect to author record */
            d_connect(HAS_PUBLISHED, task, CURR_DB);

            /* set current owner for key words and abstract */
            d_setor(INFO_TO_KEY, task, CURR_DB);
            d_setor(ABSTRACT, task, CURR_DB);

            enter_key_words(task);

            enter_abstract(task);
        }
    }
    return (0);
}      

After the data has been collected from the user (get_info), function ent_info is used to scan the author_list set for the user-specified author name. If no match is found (status == S_EOS), an author record is created and connected to author_list (the current owner of author_list is always the system record). The found or newly created author record is set as the current owner of set has_published, using d_setor. The info record is created and if its id_code is not a duplicate, the id is connected to its author record. The new info record occurrence is then made the current owner of sets info_to_key and abstract. Key words and abstract text will be connected to it by the function calls that follow.

/* Enter any key words */
static void enter_key_words(DB_TASK *task)
{
    char s[SIZEOF_KWORD];

    for (;;)
    {
        printf("key word: ");
        if (getstring(s,SIZEOF_KWORD) == NULL || s[0] == '\0')
            break;
        /* see if key word record exists */
        if (d_keyfind(KWORD, s, task, CURR_DB) == S_NOTFOUND)
        {
            /* create new key word record */
            d_fillnew(KEY_WORD, s, task, CURR_DB);
        }
        d_setor(KEY_TO_INFO, task, CURR_DB);

        /* create intersection record */
        d_fillnew(INTERSECT, &irec.info_type, task, CURR_DB);
        d_connect(KEY_TO_INFO, task, CURR_DB);
        d_connect(INFO_TO_KEY, task, CURR_DB);
    }
}

Function enter_key_words shows how to create many-to-many relationships. For each key word entered by the user, d_keyfind is called to check if it is already on file. If not, a new key_word record is created. The found (or new) key_word is then set as the current owner of the key_to_info set, and an intersect record is created, which is then connected to both the key_word record through set key_to_info and the info record through set info_to_key.

/* Enter abstract description */
static void enter_abstract(DB_TASK *task)
{
    char text_line[SIZEOF_LINE];

    for (;;)
    {
        printf("abstract: ");
        if (getstring(text_line,SIZEOF_LINE) == NULL || text_line[0] == '\0')
            return;

        d_fillnew(INFOTEXT, text_line, task, CURR_DB);
        d_connect(ABSTRACT, task, CURR_DB);
    }
}

Function enter_abstract is very simple. As each line of abstract text is entered, the text record is created and connected to the info record, which is the current owner of set abstract.

5.6 Data Modification

The functions used to modify fields, records, and sets are shown in Table 5-6.

Table 5-6 Modification Functions

Record/Key Modification Functions

d_recwrite Write contents of current record.
d_crwrite Write data in field of current record.
d_csmwrite Write data in field of current member of set.
d_csowrite Write data in field of current owner of set.
Set Modification Functions
d_discon Disconnect current member from set.
d_connect Connect current record to set.

Modification of data is very straightforward. To modify a record or field, you first retrieve the record, read the record or field contents, make the desired changes and then write out the updated data using a record modification function.

Set modifications often involve disconnecting a record from one set and connecting it to another.

If two or more fields in a record are used as sort fields in an ascending or descending set, and function d_recwrite is used to modify the contents of both fields, better performance will result if you first disconnect the record from the set before calling d_recwrite and then reconnect the record after returning from d_recwrite. Otherwise, RDM Embedded will still adjust the position of the set for each field.

Optional keys are automatically modified if a key existed for the old value when its field was modified. If a key entry did not exist for the old value, none will be created for the new value. However, the field's value in the record is updated.

5.7 Data Deletion

The functions involved in data deletion appear in Table 5-7.

Table 5-7. Data Deletion Functions
Data Deletion Functions
d_delete Delete current record.
d_keydel Delete optional key value.
d_discon Disconnect current member from set.
d_disdel Disconnect current record from all sets and delete it.

A record can only be deleted if it is not connected as an owner or member of any sets. The general deletion procedure is, therefore, to disconnect a record from all sets for which it is a member and disconnect all members from sets owned by that record, and then delete the record. All disconnections and the delete can be performed with one call by using function d_disdel.

Function d_keydel is used to delete the key entry of an optional key field in the current record. The field value in the record itself, however, does not change. All key entries, including optional keys, are deleted from their respective key files when a record is deleted using either function d_delete or d_disdel.

Below is the code for function del_info, which deletes an info record from the tims database.

#include <stdio.h> 
#include "rdm.h" 
#include "tims.h" 

/* Delete technical information records from TIMS database */
int32_t del_info(DB_TASK *task)
{
    int32_t status;
    struct info irec;
    int32_t count;
    char id[SIZEOF_ID_CODE], name[SIZEOF_NAME];

    /* find info to be deleted */
    printf("id_code: ");
    getstring(id,SIZEOF_ID_CODE);
    if (d_keyfind(ID_CODE, id, task, CURR_DB) == S_NOTFOUND)
    {
        printf("id_code %s not on file\n", id);
        return (0);
    }
    d_recread(&irec, task, CURR_DB);

    /* get author name */
    d_findco(HAS_PUBLISHED, task, CURR_DB);
    d_crread(NAME, name, task, CURR_DB);

    ... /* confirm delete request */
    ... /* disconnect any listed articles */
    ... /* disconnect and delete borrowers */
    
    /* disconnect and delete abstract */
    d_setom(ABSTRACT, HAS_PUBLISHED, task, CURR_DB);
    while (d_findfm(ABSTRACT, task, CURR_DB) == S_OKAY)
    {
        d_discon(ABSTRACT, task, CURR_DB);
        d_delete(task, CURR_DB);
    }

    /* disconnect and delete intersect and (possibly) key word */
    d_setom(INFO_TO_KEY, HAS_PUBLISHED, task, CURR_DB);
    while (d_findfm(INFO_TO_KEY, task, CURR_DB) == S_OKAY)
    {
        d_discon(INFO_TO_KEY, task, CURR_DB);
        d_setmr(KEY_TO_INFO, task, CURR_DB);
        d_discon(KEY_TO_INFO, task, CURR_DB);
        d_delete(task, CURR_DB);
        d_members(KEY_TO_INFO, &count, task, CURR_DB);
        if (count == 0L)
        {
            /* delete key word */
            d_setro(KEY_TO_INFO, task, CURR_DB);
            d_delete(task, CURR_DB);
        }
    }

    /* disconnect info record from author and delete */
    d_discon(HAS_PUBLISHED, task, CURR_DB);
    d_delete(task, CURR_DB);

    /* delete author too, if he has no other pubs */
    d_members(HAS_PUBLISHED, &count, task, CURR_DB);
    if (count == 0L)
    {
        d_setmo(AUTHOR_LIST, HAS_PUBLISHED, task, CURR_DB);
        d_discon(AUTHOR_LIST, task, CURR_DB);
        d_delete(task, CURR_DB);
    }
    return (0);
}

Function del_info first prompts the user for the id code of the info record to be deleted. If the id code is on file, the info record contents are read into variable irec, the author name is found from the owner of info through set has_published, and a confirmation of the delete is requested by displaying the data (not shown).

If the info item to be deleted is a journal or magazine, any articles contained in it must also be deleted. This is done first (not shown).

Next, any borrower records that are members of that info item are disconnected and deleted (also not shown).

The abstract is deleted by repeatedly disconnecting and deleting the first member of the abstract set until there are no more members. Note that the current owner of abstract had to be initially set to the info record that is the current member of has_published (as established by the earlier d_findco(HAS_PUBLISHED) call).

Deletion of the key words associated with the info record is similar, but more complicated. Intersect records are deleted just like the abstract was, except that they need to be disconnected from set key_to_info as well as from set info_to_key. If the key_to_info set is empty after deleting the intersect record, the key_word must also be deleted. Note the call to d_setro to set the current record from the current owner of set key_to_info (that is, the key_word record). This is necessary because function d_delete deletes the current record, and the key_word record was not the current record.

The info record is disconnected from its final set, has_published, and can now be deleted. Function d_discon disconnects the current member from the specified set and makes the disconnected member record the current record.

One last task is a check to see if the author has other info items in the database. If not, then the author record is disconnected from set author_list and is deleted as well.

5.8 Database Error Reporting

All RDM Embedded runtime functions return an integer database status code as the value of the function. These status codes are classified into the following three categories:

User Errors These correspond to programming errors, such as passing a record type to a function with a set type as its argument. User error codes range from -1 to -99.
System Errors These errors occur when RDM Embedded detects an abnormal database condition, such as no more file space. System error codes range from -900 to -999.
Function Statuses These status codes are returned to inform the program of normal function results. For example, when function d_keyfind returns code S_NOTFOUND it indicates that the key value was not in the

When user errors are reported, either the arguments to an RDM Embedded function are not correct or the database environment has not been properly set up for the called function. For example, function d_csoread will return user error S_NOCO if the current owner of the specified set is NULL_DBA. All user errors in an RDM Embedded program should be corrected.

System errors indicate that a serious error has occurred and processing should be terminated immediately. These errors are caused by improper use of pointers, by memory corruption, and by operations on strings that are not terminated by a null byte. The most common pointer error is passing a function argument by value rather than by reference (that is, rather than by using a pointer).

The standard RDM Embedded C header file, rdm.h, contains constant definitions for all status codes that can be returned from an RDM Embedded function. This file should be included in each C source file that uses RDM Embedded. Detailed explanations for each status code can be found in the RDM Embedded Reference Manual.

The internal function dberr is automatically called by the RDM Embedded runtime functions whenever a user or system error occurs. Whenever a user or system error occurs, this version of dberr will display the error code and description, and then prompt for a <Return> to continue. You may need to customize the error reporting to make it more suitable for the user interface style of your application.

The recommended method is to use a d_set_dberr function, which allows you to supply the RDM Embedded runtime with a callback function to call when reporting an error. After calling a d_set_dberr function, the dberr function will call the function you supply, instead of printing the message itself.

The dberr function that you define may have an error message string supplied in ASCII or UNICODE. Here are the two valid forms of your own dberr function (note, you choose the name):

void EXTERNAL_FCN my_dberr(int32_t, char *);     /* ASCII message   */
void EXTERNAL_FCN my_dberrW(int32_t, wchar_t *)  /* UNICODE message */

The first parameter is the error number, and the second is the textual message that dberr would have printed regarding the error. The void EXTERNAL_FCN preceding the function name ensures that its declaration will match that expected by RDM Embedded. The macros are defined in rdm.h.

Both of the above functions is supplied to the correct d_set_dberr function, as follows:

d_set_dberr(my_dberr, task);
d_set_dberrW(my_dberrW, task);

Use the function as follows:

#define "rdm.h" 

void EXTERNAL_FCN my_dberr(int32_t, char *); 
... 
main() 
{ 
   ... 
   d_set_dberr(my_dberr, task); 
   ... 
} 
void EXTERNAL_FCN my_dberr(int32_t err_no, char *err_msg) 
{ 
   /* Special handling of some error codes here */ 
   if (err_no <= -900) 
   {  
      exit(err_no); 
   } 
   ... 
   /* You may choose to ignore some codes */ 
   if (err_no == S_NOCM)  
      return; 
   ... 
   /* Report errors */ 
   ... 
   return;
} 

NOTE: if your environment requires you to free all allocated memory and otherwise clean up, the exit() call may not be a proper way to terminate the program. An internal function named psp_term() can be used to ask RDM Embedded to release its internal resources. It is a nested function, meaning that internally, RDM Embedded will always call psp_init() and psp_term() in pairs. So if you must terminate the program abnormally, you should call psp_term() five times, just to be safe (extra calls will do no extra work). See below:

/* My OS requires *everything* to be cleaned up! */ 
if (err_no <= -900) 
{ 
    /* clean up my own memory, file handles, etc. */
    ...
    /* clean up RDM Embedded resources */
    psp_term(); psp_term(); psp_term(); psp_term(); psp_term();
    exit(err_no); 
} 

5.9 Multiple Database Access

Using RDM Embedded, more than one database can be open and accessed within a single application program. This capability has been implemented with very little performance impact for those RDM Embedded applications that only need to access a single database. Logical Design Considerations described some of the uses of multiple database access from a database design standpoint. This section explains how to open and access multiple databases.

5.9.1 Opening Multiple Databases

To open more than one database, the desired database names are passed, separated by a semicolon (;) as the first argument to function d_open. After one or more database has been opened, it is possible to open additional databases with either d_iopen or d_iopen_ptr. No white space (that is, spaces, tabs, etc.) should be embedded between the database names. An improperly constructed list will result in error S_INVDB being returned. Any number of databases may be opened, limited only by the amount of available memory. All opened databases are opened in the same mode. It is not possible to open some in exclusive access and others in shared access.

RDM Embedded keeps track internally of the current database number. The current database will be zero (that is, the first one listed) after execution of d_open. See the example below:

if ((status=d_open("genledg;acctsrec;acctspay", "s", task)) != S_OKAY) { 
   if (status == S_UNAVAIL) 
      printf("database(s) not currently available\n"); 
   exit(1); 
} 

Here, the databases named genledg, acctsrec and acctspay are all opened for shared access. Each is assigned a number by the system in order from left to right, beginning with zero. Thus database 0 is genledg, database 1 is acctsrec, and database 2 is acctspay. These numbers are used to specify to the runtime functions which database to access. After the d_open call, database genledg will be the current database.

It is not necessary to open all databases at one time. The function d_iopen will incrementally open a new (set of) database(s) in the same open mode as the already opened databases. The following statements would be equivalent to the example above, except that the current database will be two instead of zero:

if ((status = d_open("genledg", "s", task)) != S_OKAY || 
    (status = d_iopen("acctsrec", task)) != S_OKAY    || 
    (status = d_iopen("acctspay", task)) != S_OKAY))  
{ 
   if (status == S_UNAVAIL) 
   { 
      printf("database(s) not currently available\n"); 
      exit(1); 
   } 
} 

Whether the databases are opened all at once, or incrementally, they may all be closed with the d_close call. They may be incrementally closed with the d_iclose call. For example, after the above code opens the database, the call below will close the acctsrec (second) database.

d_iclose(1, task); 

All database numbers will shift accordingly (in this case, the database number for acctspay will shift from two to one). If database 2 (acctspay) was the current database, then database 1 (still acctspay) will be the current database following the d_iclose.

In addition to the current database, a current record is maintained for each database. Whenever the current database is changed, the current record for the old database is saved and the current record for the database that is to be made current is restored.

5.9.2 Embedded Dictionaries

To open a database, a database dictionary is required. Most commonly, the dictionary is stored in a file on the TFS, named after the database with a ".dbd" suffix. The ddlp utility will always (unless specifically requested not to by command-line option) create and store the dictionary in the proper location on the TFS. The d_open and d_iopen calls depend on the existence of a dictionary file.

An embedded dictionary is one that has been compiled into the application program, eliminating the need for a separate dictionary file. The embedding occurs once during the development of the application, and is not required again when the application is deployed with different TFSs. Without an embedded dictionary, each new TFS will need to have the dictionary file installed in it, either through use of the ddlp utility, or through administrative copying of the file into the correct location in the TFS domain (see Operational Environment for how the location is determined). However, if an embedded dictionary is used on a TFS that doesn't already have the corresponding .dbd file, this function will cause the .dbd file to be created in the proper location (even in-memory if the databases is defined as inmemory).

The open functions that use embedded dictionaries are d_open_ptr and d_iopen_ptr.

When using an embedded dictionary, it is not possible to open multiple databases in one call. Multiple calls are needed to open multiple databases, the first call is made to d_open_ptr, and all subsequent calls are made to d_iopen_ptr.

To embed multiple databases into a program, run the ddlp utility to create additional source files:

ddlp -c genledg.ddl
ddlp -c acctsrec.ddl
ddlp -c acctspay.ddl

For each database, the DDL compilation option will generate dbname_dbd.c and dbname_dbd.h. The .c file is compiled and linked with the application program, and the .h file is included in the source file that performs the d_[i]open_ptr call.

The following code shows the same databases being opened as in the example above, except that the dictionaries are embedded:

#include "rdm.h"
#include "genledg_dbd.h"
#include "genledg.h"
#include "acctsrec_dbd.h"
#include "acctsrec.h"
#include "acctspay_dbd.h"
#include "acctspay.h"
 
if ((status = d_open_ptr("genledg", "s", &genledg_dbd, sizeof(genledg_dbd), task)) != S_OKAY || 
    (status = d_iopen_ptr("acctsrec", &acctsrec_dbd, sizeof(acctsrec_dbd),task)) != S_OKAY    || 
    (status = d_iopen_ptr("acctspay", &acctspay_dbd, sizeof(acctspay_dbd),task)) != S_OKAY))  
{ 
   if (status == S_UNAVAIL) 
   { 
      printf("database(s) not currently available\n"); 
      exit(1); 
   } 
}

5.9.3 Accessing Multiple Databases

There are two methods for accessing multiple databases. Every database-specific function must be called with a database number (dbn). The dbn may be -1, which causes the current database to be used. The rdm.h header file contains a #define constant called CURR_DB that is equal to -1. The current database may be altered by the d_iopen or d_setdb functions. Alternatively, the dbn parameter may be a positive number that explicitly selects a database. These methods may be used interchangeably.

If only one database is opened, the database number parameter is still required and must be zero (0) or CURR_DB.

Note: When a module (that is, a .c file) is to access multiple databases, care must be taken to ensure that there are no record, field, or set name conflicts between those databases, which will be manifested by the includes of multiple database header files. If there are, it will be necessary to disambiguate the names by using the -d option with ddlp, or change some of the names, or create separate modules, each of which access only the entities of a single database.

Method 1

In this method, function d_setdb is called to set the current database. The dbn parameter must be -1 (or its equivalent CURR_DB). An example of this technique follows.

#include "rdm.h" 
... 

d_open("genledg;acctsrec", "s", task); 
   ... 
/* enter billing record into acctsrec database */ 
d_setdb(1, task); 
d_fillnew(BILLING, &bill, task, CURR_DB); 

/* find and update ledger account in genledg database */ 
d_setdb(0, task); 
d_keyfind(ACCT_ID, bill.gl_id, task, CURR_DB); 
d_recread(&glacct, task, CURR_DB); 
   ..   /* update gen. ledger account record */ 
d_recwrite(&glacct, task, CURR_DB); 

Method 2

In this method, the database number is passed to the RDM Embedded functions that access or control database content. An example of this technique follows.

d_open("genledg;acctsrec", "s", task); 
   ... 
/* enter billing record into acctsrec database */ 
d_fillnew(BILLING, &bill, task, 1); 

/* find and update ledger account in genledg database */ 
d_keyfind(ACCT_ID, bill.gl_id, task, 0); 
d_recread(&glacct, task, 0); 
   ..   /* update gen. ledger account record */ 
d_recwrite(&glacct, task, 0); 

If the position of a database is not always fixed, the d_dbnum function can be used to always be sure the correct database number is used. The following example shows the same code, but with d_dbnum instead of constants:

d_open("genledg;acctsrec", "s", task); 
   ... 
acctsrec_num = d_dbnum("acctsrec", task);
/* enter billing record into acctsrec database */ 
d_fillnew(BILLING, &bill, task, acctsrec_num); 

genledg_num = d_dbnum("genledg", task);
/* find and update ledger account in genledg database */ 
d_keyfind(ACCT_ID, bill.gl_id, task, genledg_num); 
d_recread(&glacct, task, genledg_num); 
   ..   /* update gen. ledger account record */ 
d_recwrite(&glacct, task, genledg_num);

Mixed Method

It is possible to use d_setdb to set the current database, but override it with an explicit database number when necessary.

d_open("genledg;acctsrec", "s", task); 
   ... 
/* enter billing record into acctsrec database */ 
d_setdb(0, task); 
d_fillnew(BILLING, &bill, task, 1); 

/* find and update ledger account in genledg database */ 
d_keyfind(ACCT_ID, bill.gl_id, task, CURR_DB); 
d_recread(&glacct, task, CURR_DB); 
   ..   /* update gen. ledger account record */ 
d_recwrite(&glacct, task, CURR_DB); 

5.10 Multiple Tasks

An application always has a current state. This includes such factors as which database is open, the current record, current set owners and members, record and set locks, and many other items that represent the situation of the application. Below is a list of the most significant task-specific data:

A task is an RDM Embedded structure used to store state information, and is created by a call to d_opentask. The one parameter to this function is a pointer to a structure of type DB_TASK. The d_opentask function will allocate a structure to contain all of the task-specific state information and point the task pointer to it. The d_closetask function should always be called to clean up and free all memory allocated by d_opentask.

#include "rdm.h" 
  
main() 
{ 
   DB_TASK *task; 

   d_opentask(&task);         /* New task */ 

   d_open("db", "x", task);   /* Open database in task */ 
      /* Use the database */ 
   d_close(task); 

   d_closetask(task);         /* Close task, free memory */ 
} 

The DB_TASK parameter must be supplied for nearly every RDM Embedded function, and at least one call must be made to d_opentask. The task parameter is placed before the database number parameter for those functions that use a database number. Otherwise, it is the last argument.

An RDM Embedded task equates to an independent unit of code execution, or it could also be referred to as a session. In addition to maintaining state information, each task may represent a different transaction. This is a deliberately broad definition. You can use multiple database tasks within a single process. You also may use multiple tasks in multiple threads, provided that no two threads use common tasks.

The RDM Embedded functions are thread-safe, provided each task is used within only one thread.

A separate task can be opened for the purpose of accessing additional databases in a different open mode. For example, you may have a database open in shared mode, but then want to open an additional database exclusively.

The following code shows a simple single-process application using two contexts.

#include "rdm.h" 

DB_TASK *ControlTask; 
DB_TASK *ClientTask; 

main() 
{ 
   struct client cl2; 
   d_opentask(&ControlTask); 
   d_open("ctrl", "x", ControlTask); 
   /* Initialize control database */ 
   ... 
   d_opentask(&ClientTask); 
   d_open("client1;client2;client3", "s", ClientTask); 
   ... 
   /* Access client3 database */ 
   d_fillnew(CLIENT, &cl2, ClientTask, 2); 
   ... 
   d_close(ClientTask); 
   d_closetask(ClientTask); 
   ... 
   d_close(ControlTask); 
   d_closetask(ControlTask); 
} 

In this example, two variables of type DB_TASK* are defined. One task is created to open a single control database in exclusive access mode. Then a second task is created to open a group of shared databases, each representing one client. One or more client database may be updated by a transaction within the client task.

There are two functions that do not take a task parameter. They are d_decode_dba and d_encode_dba.

5.11 Database Unions

NOTE: The Database Union functionality requires either the Distributed or Flow packages of RDM Embedded. See Packages in RDM Embedded.

A database union is a unified view of the data in more than one identically structured database. It makes the multiple databases appear as one. A union of multiple databases differs from having multiple databases open in that:

Database unions are intended to be used with distributed databases or database mirrors to create a single, merged view of data that is owned and updated in separate locations or by separate entities.

Distributed Operation

Consider a corporate phone list where each corporate office maintains its own list of employees at the office where they work:


Fig. 5-5 Distributed Phone List

Assuming that the salesman in Wichita can establish a VPN with the computers named seattle, boston and dallas, then the following program can be run from there on non-unioned databases:

/* print the names in this phlist database */
static void print_list(DB_TASK *task)
{
    struct employee empl;
    int32_t rc;
 
    printf("LAST    FIRST   EXT\n");
    for (rc=d_keyfrst(LAST, task, 0); rc==S_OKAY; rc=d_keynext(LAST, task, 0))
    {
        d_recread(&empl, task, 0);
        printf("%-7s %-7s %3d\n", empl.last, empl.first, empl.ext);
    }
}
 
/* print phone list contents separately */
main() {
    ...
    d_open("phlist@seattle", "r", task);
    print_list(task);
    d_close(task);
 
    d_open("phlist@boston", "r", task);
    print_list(task);
    d_close(task);

    d_open("phlist@dallas", "r", task);
    print_list(task);
    d_close(task);
    ...
}

The output would be as follows:

LAST    FIRST   EXT
Bell    Eric    529
Klaus   Ted     598
Lee     Jeff    523
Marsh   Rich    567

LAST    FIRST   EXT
Ellern  Karen   677
Kent    Ian     612
Ward    Ralph   643

LAST    FIRST   EXT
Clark   Susan   408
Devin   Paul    445

The union of these three databases is represented by a string listing the names separated by the OR bar, "|". After opening the union, the navigation and reading functions do not need to know that they are viewing the contents of more than one database. The following code opens a union in order to read the same lists as one list:

/* print the names in this phlist database */
static void print_list(DB_TASK *task)
{
    ...
}
 
/* print phone list contents together */
main() {
    ...
    d_open("phlist@seattle|phlist@boston|phlist@dallas", "r", task);
    print_list(task);
    d_close(task);
    ...
}

The output of the key scanning is a list where the keys are still in the same order:

LAST    FIRST   EXT
Bell    Eric    529
Clark   Susan   408
Devin   Paul    445
Ellern  Karen   677
Kent    Ian     612
Klaus   Ted     598
Lee     Jeff    523
Marsh   Rich    567
Ward    Ralph   643

Mirrored Operation

NOTE: Only the Flow package contains both Database Union and Mirroring functionality. See Packages in RDM Embedded.

The above example illustrates how "live" data can be accessed from its sources. But a phone list is not updated frequently, nor it is extremely large, so it is a perfect candidate for mirroring.

If each of the three offices mirror the phone lists from the other two, then each office will have three local databases - one is their own updateable list, and two read-only mirrors. Each mirror would be kept up-to-date whenever there is a change in a master list from another office. This means that a union of three local databases will produce the same results as the distributed form shown in Figure 5-5.

The following shows a list of the databases that are available at each location:

\\seattle\document-root:
    phlist\
    boston-21553\
        phlist\
    dallas-21553\
        phlist\

\\boston\document-root:
    phlist\
    seattle-21553\
        phlist\
    dallas-21553\
        phlist\

\\dallas\document-root:
    phlist\
    seattle-21553\
        phlist\
    boston-21553\
        phlist\

For a program running within the seattle computer, the following open statement would allow queries on the entire phone list:

d_open("phlist|boston-21553/phlist|dallas-21553/phlist", "r", task);

Similarly, an application running within dallas would use:

d_open("phlist|seattle-21553/phlist|boston-21553/phlist", "r", task);

The salesman in Wichita will have several options. One option is to go directly to each of the databases, as shown in the distributed example above. A second option is to select a nearby office (say "Dallas") and open the master and mirrors on that same machine. The first example below shows the use of the RDME_TFSERVER environment variable, the second shows the equivalent form when encoding the TFS into the database names:

c:\RDMe> set RDME_TFSERVER=dallas
c:\RDMe> phlist  {which executes
   ...
   d_open("phlist|seattle-21553/phlist|boston-21553/phlist", "r", task);
   ...
   }

or

d_open("phlist@dallas|seattle-21553/phlist@dallas|boston-21553/phlist@dallas", "r", task);

Key Navigation

The d_keyfind function will find the requested key in any of the unioned databases. If the key exists, the function will return S_OKAY and set the found record to be the current record. It is irrelevant which database the record came from. If the key does not exist, d_keynext or d_keyprev can be called to find the next or previous key, closest to the key that was provided in the d_keyfind call. This is the same behavior as with a single database. Following a d_keyfind, either d_keynext or d_keyprev will move to the next or previous key, regardless of which database it comes from. It is, in effect, a "sort/merge" of the keys of all databases.

Some databases have unique keys. When these databases are in a union, there may be duplicates among the unique keys, because the keys are unique in their own database, but no cross-checking is automatically done among all the databases when keys are inserted. Intrinsically unique keys like a SSN should not be a problem, but a part-number may be unique in one store, and duplicated in other stores. This kind of situation must be anticipated if converting a program to work on a union after being programmed for a single database.

Record Scanning

The functions d_recfrst, d_reclast, d_recnext, d_recprev and d_recset perform sequential scanning of the records of one type. When these functions are used, it is implied that the order doesn't matter, because RDM Embedded does not guarantee anything about ordering.

When using these functions in a database union, a complete scan from one end to the other will be guaranteed to return all records in all databases, but the order of retrieval will be based on an internal algorithm.

Set Scanning

Set connections between records will only exist between records in the same database. The set navigation functions will never span different databases in a union. Thus a set will never appear any larger when the database containing it is in a union.

Updates

Database unions are read only. If updates to a database are necessary, a program may open a database within another task (using the d_opentask function), in shared mode, to perform the update

5.12 BLOB Processing in RDM Embedded

RDM Embedded stores BLOB data in a separate file from the record that contains the BLOB data field. In the DDL, a file must be declared to be a "blob file", which will store BLOB fields that are listed in the file declaration. Within the file, a BLOB is stored as a linked list of fixed-length pages. You can specify the page size for each BLOB file in the DDL.

A BLOB is not physically stored together with the record that contains it. The record containing the BLOB data field stores a number pointing to the BLOB's first page in its file. The page number is stored as an F_ADDR in the record, which means that the record structure is fixed length. A BLOB is associated with only one record instance.

There are no special locking requirements associated with accessing BLOB data. Because of the one-to-one relationship between a record and its associated BLOB data, the usual record locks secure access to both the record occurrence and the BLOB data.

The RDM Embedded Core API functions for manipulating BLOB data model the standard C file I/O functions: read, write, seek, size, and tell. One significant difference to note between the two, however, is that BLOBs are organized as a linked list and, therefore, random access does not perform like direct access (i.e., d_blobseek will not be as fast as seek because it needs to scan a linked list to arrive at the specified location).

5.12.1 Working with BLOBs in a Core API Application

Define BLOB Data in a Core DDL Specification

A BLOB is associated with a record type by declaring a data field of type blob_id. The ID field designated by the blob_id type contains an F_ADDR equal to the page number in the field's BLOB file where the BLOB data begins.

blob_id pictograph;

You may not define an index for a BLOB ID field.

The BLOB functions operate on the blob_id fields contained in the current record.

The following Core DDL specification shows some example BLOB declarations.

database musiclib {
    data file "musiclib.000" contains CDAlbum;
    data file "musiclib.001" contains track;
    blob file[16384] "musiclib.002" contains mp3;
    blob file[512] "musiclib.003" contains notes, jacketpic;
    key  file[2048] "musiclib.004" contains title;

    record CDAlbum {
        key char    title[81];
            char    composer[33];
            char    artist[33];
            char    genre[21];
            blob_id notes;
            blob_id jacketpic;
    }

    record track {
            char    name[81];
            blob_id mp3;
    }

    set CDTracks {
        order  last;
        owner  CDAlbum;
        member track;
    }
}

The CDAlbum record has two BLOB fields. The jacketpic field is used for storing a picture of the album jacket. Field notes stores the text of a note about the album. Data field mp3 in the track record type is used to store the music for a given track in MP3 format. Note however, that RDM Embedded does not recognize file formats such as MP3 or JPG in order to perform special handling according to their type.

Two BLOB files are used to contain the BLOB data. The first one has a page size of 16384 bytes and is used to contain the MP3 formatted music. The music for a given track will tend to be several megabytes in length, hence the large page size. The second BLOB file is used to contain the text of notes and the picture image for jacketpic.

These BLOBs will typically be much smaller so the specified page size is only 512 bytes and contains the BLOB data for both blob_id fields.

Manipulate BLOB Data Using the Core API Functions

The Core BLOB functions are summarized in the Reference Manual in the Core BLOB API Summary section.

These BLOB functions all operate on a blob_id field (specified as a BLOB function argument) in the current record. When a record instance is set as the new value of the current record, the initial position for all blob_id fields in that record is set to the beginning of the BLOB. Until the current record is changed to a different record instance, the BLOB positions are controlled completely through calls to the Core BLOB functions.

A call to the d_crread function of a blob_id field returns the blob_id value. However, the contents of a blob_id field can only be manipulated by the d_blob functions themselves. You can, for example, write into the middle of a BLOB by doing first a d_blobseek and then a d_blobwrite. If you attempt to use d_recwrite, d_crwrite, d_csowrite, or d_csmwrite to update a blob_id field, the S_BLOBUPD error message is returned.

A few additional facts concerning the operation of BLOBs:

The example below shows a function called PlayCD that looks up the CD with the specified title and plays each of its music tracks. After each track is retrieved from the CDTRACKS set, d_blobread is called to read the MP3 BLOB data one bufsize byte block at a time.

/* ==================================================================
   Play all tracks from specified CD
*/
int32_t PlayCD(
    char    *title,   /*title of CD */
    uint32_t bufsize, /* size of music buffer to use */
    DB_TASK *task)    /* task handle for "musiclib" */
{
    char name[SIZEOF_NAME], *mbuf;
    uint32_t size;
    int32_t  stat;

    if ((stat = d_keyfind(TITLE, title, task, CURR_DB)) != S_OKAY)
        return stat;

    /* allocate music buffer */
    mbuf = psp_getMemory(bufsize, 0);

    /* read and play each track for this CD */
    d_setor(CDTRACKS, task, CURR_DB);
    while (d_findnm(CDTRACKS, task, CURR_DB) == S_OKAY)
    {
        d_crread(NAME, name, task, CURR_DB);
        ShowTrackName(name);
        while (d_blobread(MP3, mbuf, bufsize, &size, task, CURR_DB) == S_OKAY)
        {
            PlayMusicBlock(mbuf, size);
            if (size < bufsize)
                break;
        }
    }

    psp_freeMemory(mbuf, 0);
    return stat;
}

Storing a BLOB is just about as easy as reading one. The following code shows a function that is passed the name of a file containing an MP3 music track and the title of the CD in which it is included. The function reads the music data from the file and stores it in the database as a BLOB.

/* ==================================================================
   Store a track from specified CD */
int32_t StoreCDTrack(
    char     *title,     /* title of CD */
    char     *filename,  /* name of file containing MP3 music */
    uint32_t  bufsize,   /* size of music buffer to use */
    DB_TASK  *task)      /* task handle for "musiclib" */
{
    struct  track rTrack;
    FILE   *fMp3;
    char   *mbuf;
    size_t  size;
    int32_t stat;

    if ((stat = d_keyfind(TITLE, title, task, CURR_DB)) != S_OKAY)
        return stat;

    if ((fMp3 = fopen(filename, "rb")) == NULL)
        return S_NOFILE;

    /* set the CDAlbum record as owner of set cdtracks */
    d_setor(CDTRACKS, task, CURR_DB);

    /* create track record, use the filename as the track name */
    strncpy(rTrack.name, filename, SIZEOF_NAME-1);
    rTrack.name[SIZEOF_NAME-1] = '\0';
    rTrack.mp3 = 0;

    if ((stat = d_trbegin("StoreCDTrack", task)) != S_OKAY)
        return stat;

    d_fillnew(TRACK, &rTrack, task, CURR_DB);
    d_connect(CDTRACKS, task, CURR_DB);

    /* allocate music buffer */
    mbuf = psp_getMemory(bufsize, 0);

    while ((size = fread(mbuf, 1, 65536, fMp3)) > 0) {
        if ((stat = d_blobwrite(MP3, mbuf, size, task, CURR_DB)) != S_OKAY) {
            psp_freeMemory(mbuf, 0);
            fclose(fMp3);
            d_trabort(task);
            return stat;
        }

        if (size < 65536)
            break;
    }

    psp_freeMemory(mbuf, 0);
    d_trend(task);
    fclose(fMp3);

    return stat;
}

The next code sample illustrates the use of d_blobseek and d_blobtell. It plays a music track from a pre-specified position until either the play is completed or the play is paused. The position is passed in through the pTrackPos argument that points to an integer containing the start position (*pTrackPos = 0 to play an entire track). If the play is paused, the current position is stored back in *pTrackPos.

The d_blobseek function is called to position the BLOB to the specified starting position. Then bufsize bytes of the music track BLOB is read and passed into a function called PlayMusicBlock, which plays the block of music. If the user pauses the play, PlayMusicBlock returns the offset into the block where it paused, otherwise it returns 0. When paused, the d_blobtell function is called to get the current BLOB position, which is used to compute the actual offset from the start of the BLOB to the paused position.

/* ==================================================================
   Resume play at specified position on track
*/
int32_t ResumePlay(
  DB_ADDR   dbaTrack,    /* db addr of track record */
  int32_t  *pTrackPos,   /* pointer to current offset */
  uint32_t  bufsize,     /* size of music buffer to use */
  DB_TASK  *task)        /* task handle for "musiclib" */
{
    /* returns 0 if play is paused, 1 when play is complete */
    char    *mbuf;
    uint32_t paused, pos = *pTrackPos;
    uint32_t size;
    int32_t  done = 1;

    /* allocate music buffer */
    mbuf = psp_getMemory(bufsize, 0);

    /* reset current record to desired track */
    d_crset(&dbaTrack, task, CURR_DB);

    /* position BLOB to where last left off */
    d_blobseek(MP3, pos, B_START, task, CURR_DB);

    /* read next block of music from BLOB */
    while (d_blobread(MP3, mbuf, bufsize, &size, task, CURR_DB) == S_OKAY) {
        paused = PlayMusicBlock(mbuf, size);
        if (size < bufsize) {
            *pTrackPos = 0;
            break;  /* we're done */
        }

        if (paused) {
            /* fetch current BLOB position */
            d_blobtell(MP3, pTrackPos, task, CURR_DB);

            /* adjust for where in current block music was paused */
            *pTrackPos -= size-paused;

            done = 0;
            break;
        }
    }

    psp_freeMemory(mbuf, 0);
    return done;
}

As mentioned earlier, BLOBs are stored in the blob file in a linked list of fixed-length pages. Thus an important distinction to remember between the RDM Embedded BLOB functions and the low-level C file operations is that, whereas the C seek file operation performs direct, random access to the specified file position, the d_blobseek function must scan through the linked list to locate the desired position within the BLOB. This can have a negative performance impact were you to use an RDM Embedded BLOB in the same way you would a random access file.

You can delete a record by using a call to the d_delete function. A d_delete of a record instance containing a BLOB deletes, along with the record instance, all BLOBs contained in the record. To delete only the BLOB data from the current record, call d_blobdelete.


Copyright © 2011, Raima Inc. All rights reserved.