9.15 More File Input-Output

This section is devoted to the remaining file statements. These statements allow you to create new records, remove old records, and change existing records in place. Each statement is presented individually, showing how each one works by itself. Then, at the end of this section, a transaction processing program for the demo system is presented.

The first statement is the UPDATE statement. With this statement, you can change the contents of any record in an RMSfile. Before you can change the record, you must first read it. This can be done with the FIND statement, or either of the file reading statements: FOR or READ. After the record is read, you can make changes to the fields in the file. Once all of the changes are made, execute UPDATE to store the changes into the RMSfile. This is very important. If UPDATE is not executed, the changes to the fields are not stored into the RMSfile. The following statements find the magazine with the code "sd" and change the title to "Science Digest".

FIND IN mag

WHERE magazine EQ "sd"

title := "Science Digest"

UPDATE mag;

These statements make several assumptions, not all of which are necessarily true. The first is that the FIND actually finds what we are looking for. As long as the record exists in the RMSfile, this should be the case. If we need to be sure, we can check to see if any errors existed for the file mag. This would tell us if we found the record. The other assumption made is that the update process actually worked. Again, we have to check for errors to be sure of that.

File operation errors are returned to a report program through the ERROR function. This function is similar to the end of file function from the previous section. A call to ERROR looks like.

ERROR ( fname )

where fname is an internal file name declared at the beginning of the report program. This function returns zero (0) if no errors occurred during the file operation. If an error did occur, the ERROR function returns the error number from RMS.

If we were to incorporate error checking into our example above, we might have something like

FIND IN mag

WHERE magazine EQ "sd"

IF ERROR (mag) NE 0 THEN

PRINT "No magazine to update", NL;

ELSE BEGIN

title := "Science Digest"

UPDATE mag;

IF ERROR (mag) NE 0 THEN

PRINT "Update failed", NL;

END

As you can see, error checking tends to make the program much more complex. One thing can be done to make things a little simpler. If you re-check Chapter 10, Report Writer Programming Reference, it states that a condition is false if the result is zero and assumed to be true for any other values. It just so happens the ERROR returns a zero (false) if no errors have occurred and something non-zero (true) if there were errors (actually, it was planned that way). This means that we can simplify our conditions to just checking the value returned by ERROR. This yields the following statements:

FIND IN mag

WHERE magazine EQ "sd"

IF ERROR (mag) THEN

PRINT "No magazine found", NL;

ELSE BEGIN

title := "Science Digest"

UPDATE mag;

IF ERROR (mag) THEN

PRINT "Update failed", NL;

END

The next statement on our list is DELETE. With this statement, you can remove records from an RMSfile. As with UPDATE, you must first read the record before you can remove it. Again, any of the file reading statements can be used to locate the desired record. The following statements remove the magazine whose code is "sd":

FIND IN mag

WHERE magazine EQ "sd"

DELETE mag;

This example makes the same assumptions that the first example on UPDATE made. The error-checking version of this looks like

FIND IN mag

WHERE magazine EQ "sd"

IF ERROR (mag) THEN

PRINT "Magazine not found", NL;

ELSE BEGIN

DELETE mag

IF ERROR (mag) THEN

PRINT "Delete failed", NL;

END

The final file statement is INSERT. This statement is used to add new records to an RMSfile. Before this statement can be executed, you must assign each field its new value. When INSERT executes, it stores the contents of the fields into the RMSfile in a new record. (Note: The Report Writer does not enforce uniqueness of key values. If this is required (as in a master file) it is up to the report program to insure this. The following statements add a new magazine with the code "sd", and title of "Science Digest":

magazine := "sd"

title := "Science Digest"

INSERT mag;

Error conditions were ignored in this example. Checking for errors results in the following statements:

magazine := "sd"

title := "Science Digest"

INSERT mag;

IF ERROR (mag) THEN

PRINT "Insert failed.", NL;

Some RMSfiles are set up with a fixed number of records that can be stored in the file. If you try to add a record to one of these RMSfiles and the file already contains its allocated number of records, you will get an error which means "file is already full". The function FILEFULL has been provided to eliminate the need for a report to test for a particular error number. As with the other file functions, this function also requires an internal file name as its argument. The value returned by FILEFULL is true if the RMSfile was full before the insert and false otherwise. The following statements illustrate the use of this function:

magazine := "sd"

title := "Science Digest"

INSERT mag;

IF FILEFULL (mag) THEN

PRINT "Magazine file full.", NL;

ELSE IF ERROR (mag) THEN

PRINT "Insert failed.", NL;

Now that you have seen how each of these statements is used individually, a complete example is presented that uses all of the input-output statements together. To do this, we must go outside of the demo system a little. On top of the demo system, we will add a transaction processing system. In this system, the user creates a transaction file which gets applied to the master file. These transaction files have the same format as the master files except that we have added a field that indicates what kind of transaction the record represents. This transaction code has three values:

"I" - Insert the record into file

"U" - Update the record contents

"D" - Delete the record from the file

The report uses two files: the master file, and the transaction file. The declarations for these files are:

FILE master IS "sub"

FILE tr IS "sub_tr"

FIELDS IN master ARE

subscriber name address,

city, state, zip;

FIELDS IN tr ARE

tr_code, subscriber, name,

address city, state

zip;

The main process of the report reads through the transaction file and processes each transaction. Our first try at the main procedure looks like:

MAIN

BEGIN

FOR EACH tr

IF tr_code EQ "I" THEN

DO tr_insert;

ELSE IF tr_code EQ "U" THEN

DO tr_ update;

ELSE IF tr_code EQ "D" THEN

DO tr_delete;

ELSE

PRINT "Invalid transaction code:",

tr_code, NL;

END

The sequence of statements IF ...ELSE IF is a way of making a multiway decision. In this case, we are going through a series of tests looking for a valid transaction code. When we find a valid code, we call the appropriate procedure. If we get to the bottom of the ELSE IF chain, the transaction code is invalid and we print out the error.

Now imagine this program running. You have a file of 5000 transactions being applied to the master file. Somewhere in this file, the operator has made a mistake in entering the transaction code and entered a "F" instead of a "D" (they are close on the keyboard). The error that is displayed is:

Invalid transaction code :F

It is now up to the operator to search through the 5000 transactions to find the one that has the code "F". It would be more helpful if we somehow tell the operator where the error is. One simple way is to give a transaction number. Such an error message might read

Invalid transaction code :F, transaction # 2409.

Now the operator knows exactly which transaction to change. Keeping track of the transaction number requires that we create a variable which acts as a transaction counter. Thus our new main procedure is

MAIN

BEGIN

tr_num := 1;

FOR EACH tr BEGIN

IF tr_code EQ "I" THEN

DO tr_insert;

ELSE IF tr_code EQ "U' THEN

DO tr_update;

ELSE IF tr_code EQ "D" THEN

DO tr_delete;

ELSE BEGIN

PRINT "Invalid transaction code:",

tr_code;

DO print_tr;

END

tr_num := tr_num + 1;

END

END

We are now ready to move on to the remaining procedures. In each of these procedures, we assume that the transaction record contains the new image of what is to be stored in the data file. Since both the update and insertion procedures need to move this data into the master file fields, we have created another procedure, called move_to_master, to move the fields. This procedure looks like

PROCEDURE move_to_master /* move fields to master file */

BEGIN

master.subscriber := tr.subscriber;

master.name := tr.name;

master.address [1] := tr.address [1];

master.address [2] := tr.address [2];

master.city := tr.city;

master.state := tr.state;

master.zip := tr.zip;

END

The code for insertions must do the following:

Check to make sure the subscriber code is unique.

Move the fields to the master file.

Add the record to the data file.

Check for insertion errors.

To check for uniqueness, we can try to find the subscriber code we want to add. If we do manage to find the record in the master file, then the subscriber code is non-unique. This check might look something like:

FIND IN master

WHERE subscriber EQ tr.subscriber;

IF ERROR (master) EQ 0 THEN BEGIN

PRINT "Subscriber '", master.subscriber,

"' already exists"

DO print_tr;

END

Moving the fields to the master file is a procedure call to move_to_master. Executing the INSERT statement adds the record to the master file. When checking for errors, we should make two checks. The first check is for "file full". This gives a little more meaningful error as to why an insert transaction failed. The second check can be for other errors in general. In summary, our procedure for insertions is:

PROCEDURE tr_insert /* add new subscriber */

BEGIN

FIND IN master

WHERE subscriber EQ tr.subscriber;

IF ERROR (master) EQ 0 THEN BEGIN

PRINT "Subscriber '", master. subscriber,

"' already exists"

DO print_tr;

END

ELSE BEGIN

DO move_to_master;

INSERT INTO master;

IF FILEFULL (master) THEN BEGIN

PRINT "File full on subscriber`",

master.subscriber;

DO print_tr;

END

ELSE IF ERROR (master) THEN BEGIN

PRINT "Insert failed"

DO print_tr;

END

END

END

The procedures for updates and deletions are similar to insertions. However, in these procedures it is an error if the transaction subscriber cannot be found in the master file. (Cannot change or remove a record if it is not there.) The full program follows:

/* process subscriber transaction file */

FILE master IS "sub"

FILE tr IS "sub_tr"

FIELDS IN master ARE

subscriber, name, address,

city, state, zip;

FIELDS IN tr ARE

tr_code, subscriber, name,

address, state, city,

zip;

VARIABLE IS

tr_num; /* transaction counter */

MAIN

BEGIN

FOR EACH tr BEGIN

IF tr_code EQ "I" THEN

DO tr_insert;

ELSE IF tr_code EQ "U" THEN

DO tr_update;

ELSE IF tr_code EQ "D" THEN

DO tr_delete;

ELSE BEGIN

PRINT "Invalid transaction code:",

tr_code;

DO print_tr;

END

tr_num := tr_num + 1;

END

END

PROCEDURE tr_insert /* add new subscriber */

BEGIN

FIND IN master

WHERE subscriber EQ tr.subscriber;

IF ERROR (master) EQ 0 THEN BEGIN

PRINT "Subscriber `", master.subscriber,

"' already exists"

DO print_tr;

END

ELSE BEGIN

DO move_to_master;

INSERT INTO master;

IF ERROR (master) THEN BEGIN

PRINT "Insert failed"

DO print_tr;

END

END

END

PROCEDURE tr_update /* change old subscriber */

BEGIN

FIND IN master

WHERE subscriber EQ tr.subscriber;

IF ERROR (master) THEN BEGIN

PRINT "Subscriber `", master.subscriber,

"' not found"

DO print_tr;

END

ELSE BEGIN

DO move_to_master;

UPDATE IN master;

IF ERROR (master) THEN BEGIN

PRINT "Update failed"

DO print_tr;

END

END

END

PROCEDURE tr_delete /* remove old subscriber */

BEGIN

FIND IN master

WHERE subscriber EQ tr.subscriber;

IF ERROR (master) THEN BEGIN

PRINT "Subscriber `", master.subscriber,

"' not found"

DO print_tr;

END

ELSE BEGIN

DELETE FROM master;

IF ERROR (master) THEN BEGIN

PRINT "Delete failed"

DO print_tr;

END

END

END

PROCEDURE move_to_master /* move fields to master file */

BEGIN

master.subscriber := tr.subscriber;

master.name := tr.name;

master.address [1] := tr.address [1];

master.address [2] := tr.address [2];

master.city := tr.city;

master.state := tr.state;

master.zip := tr.zip;

END

PROCEDURE print_tr /* print transaction number */

BEGIN

PRINT ", transaction # ", tr_num, NL;

END