Welcome to OpenEdge ABL Refresher

Describe the difference between compile-time versus run-time execution?


In OpenEdge ABL you execute a procedure using the RUN statement. However, you can use two basic techniques to compile and execute a procedure:

Compile-time execution — You can compile the procedure file on the fly when you execute it using the RUN statement. In this case, OpenEdge locates the procedure file, compiles it to a temporary r-code file, then executes the temporary r-code file.

Run-time execution — You can generate an r-code file from the procedure file using the COMPILE statement or the Application Compiler in the ADE. Then, when you execute the procedure with the RUN statement, OpenEdge locates and executes the r-code file directly.

Both techniques use the PROPATH environment variable settings. That is, both the COMPILE and RUN statements locate procedures using PROPATH.
How the RUN statement executes a procedure (compile-time or run-time execution) depends on how you set up your environment and when you choose to compile the procedure. Each technique provides different capabilities and advantages.

Compile-time Execution


The RUN statement performs compile-time execution when OpenEdge cannot find an existing r-code file that corresponds to the specified procedure. Because r-code is interpreted, there is no need to link an additional machine executable; the RUN statement can thus execute the procedure immediately after compiling it. Compile-time execution has two main uses: 1) Rapid prototyping and testing and 2) Dynamic ABL Code generation.

Run-time Execution


Compile-time execution can save time in development. However, to improve application performance, the first step is to ensure that it is running in r-code form. Thus, run-time execution has two main uses: 1) Application fine tuning and deployment and 2) Optimized dynamic code generation.

Describe Persistent and Non-persistent Procedures.


An external procedure can be either non-persistent or persistent. A non-persistent procedure creates and maintains its context only until it returns from execution. In other words, the context of a non-persistent procedure remains in scope only until the RUN statement that executes it completes. An external procedure is non-persistent by default.

A persistent procedure creates its context when it executes and then maintains that context after it returns until the end of the OpenEdge session, or until it is explicitly deleted. In other words, the context of a persistent procedure remains in scope after the RUN statement that executes it completes until you remove it. Thus, as long as its context is in scope, the triggers and internal procedures of a persistent procedure remain available for execution by your application. An external procedure creates a persistent context when you execute it using a RUN statement with the PERSISTENT option.

NOTE: If you run an application that creates persistent procedures from an ADE tool, that tool (for example, the Procedure Editor or User Interface Builder) removes all instances of persistent procedures still created when the application terminates.

Advantages of Persistent Procedures
Persistent procedures promote modular application design and development by more easily allowing you to distribute functionality among several procedures. For example, you might build a persistent procedure that provides access to a database through a set of local buffers that it otherwise hides from the rest of the application. Or your persistent procedure might create and manage its own windows while allowing independent access (non-modal access) to other windows in your application. Aside from helping to manage functionality, the additional modularity also helps to avoid hitting r-code segment limits.

Thus, creating a persistent procedure whose context you access through internal procedures provides the most effective means to achieve encapsulation in OpenEdge.

You can also use a persistent procedure to create multiple versions of the same context. Each time you call a persistent procedure, it creates a separate instance of its context. Your application (and user) can access and manage each context independently from the others. If your persistent procedure manages its own windows, you can use this feature to provide some (but not all) of the capabilities of the Microsoft multiple document interface (MDI).

Describe OpenEdge transaction mechanics.


The next two sections summarize the mechanics of transactions and sub-transactions.

Transaction Mechanics


During a transaction, information on all database activity occurring during that transaction is written to a before-image (BI) file. OpenEdge maintains one BI file for each database. The information written to the before-image file is carefully coordinated with the timing of the data written to the actual database table. That way, if an error occurs during the transaction, OpenEdge uses this before-image file to restore the database to the condition it was in before the transaction started. Information written to the before image file is not buffered. It is written to disk immediately.

Space in the before-image file is allocated in units called clusters. OpenEdge automatically allocates new clusters as needed. (You can use the PROUTIL TRUNCATE BI utility to set the cluster size.) After all changes associated with a cluster have been committed and written to disk, OpenEdge can reuse the cluster. Therefore the disk space used by the before-image file depends on several factors including the cluster size, the scope of your transactions, and when physical writes are made to the database (.db) file.

Subtransaction Mechanics


If a transaction is already active and OpenEdge encounters a DO ON ERROR, DO TRANSACTION, FOR EACH, REPEAT, or procedure block, OpeEdge starts a subtransaction.
All database activity occurring during that subtransaction is written to a local-before-image(LBI) file. OpenEdge maintains one LBI file for each user. If an error occurs during the subtransaction, OpenEdge uses this local-before-image file to restore the database to the condition it was in before the subtransaction started. OpenEdge uses the local-before-image file to back out variables and to back out subtransactions in all cases when an entire transaction is not being backed out.
Note that the first time a variable is altered within a subtransaction block, all of the variables in the procedure are written to the LBI file as a record.
Because the local-before-image information is not needed for crash recovery, it does not have to be written to disk in a carefully synchronized fashion as does the before-image information.
This minimizes the overhead associated with subtransactions. The local-before-image file is written using normal, buffered I/O.
The amount of disk space required for each user’s LBI file depends on the number of subtransactions started that are subject to being undone.

Efficient Transaction Processing


Here are a few guidelines to improve the efficiency of transaction processing procedures:
If you are doing extensive calculations with variables, and you do not need to take advantage of undo processing for those variables, use the NO–UNDO option when defining the variables.
If you are processing array elements, process them in a DO WHILE block rather than in a REPEAT WHILE block. That way, you will not start a separate transaction or subtransaction for each array element.
When the logic of your application permits, do as much processing as possible directly at the transaction level rather than creating subtransactions. This principle should not restrict the way you implement your application, but you should use it whenever it is convenient.

How OpenEdge Applies Locks?


We are aware that we can apply a lock on our own (using EXCLUSIVE-LOCK or SHARED-LOCK). But if you do not apply any locks, OpenEdge performs default locking. In particular:

Whenever it reads a record, OpenEdge puts a SHARE–LOCK on that record. (An exception is the browse widget). This means that other users can read the record but cannot update it until the procedure releases the SHARE–LOCK. If you try to read a record with a SHARE–LOCK when another user has that record EXCLUSIVE–LOCKed, OpenEdge displays a message that the record is in use and you must wait to access it.

Whenever OpenEdge updates a record, it puts an EXCLUSIVE–LOCK on that record. This means that other users cannot read or update that record until the procedure releases EXCLUSIVE–LOCK. If you try to read a record with an EXCLUSIVE–LOCK, when another user has that record SHARE–LOCKed or EXCLUSIVE–LOCKed, you receive a message that the record is in use and you must wait to access it.

NOTE: SHARE–LOCKs and EXCLUSIVE–LOCKs use up entries in the lock table. The possible number of entries in the lock table defaults to 8192. You can change this with the Lock Table Entries (–L) startup parameter. OpenEdge stops a user program if it attempts to access a record that overflows the lock table.

Describe Progress OpenEdge Database Events


Database triggers associate a table or field with a database event. When the event occurs, the trigger executes.

Progress OpenEdge does not provide events for all database actions. For example, although you can dump database definitions from a database, you cannot write a trigger for a DUMP event, because Progress OpenEdge does not provide a DUMP event.
However, Progress OpenEdge does provides replication-related triggers in addition to standard triggers for certain events. Replication-related triggers help you implement database replication.
The database events that Progress OpenEdge supports, along with the standard triggers and replication-related triggers are:

CREATE - When Progress OpenEdge executes a CREATE or INSERT statement for a database table, Progress OpenEdge creates the record, fires all applicable CREATE triggers, and then fires all applicable REPLICATION–CREATE triggers.

DELETE - When Progress OpenEdge executes a DELETE statement for a database table, Progress OpenEdge fires all applicable DELETE triggers, fires all applicable REPLICATION–DELETE triggers, validates the delete, and then performs the delete.

FIND - When Progress OpenEdge reads a record in a database table using a FIND or GET statement or a FOR EACH loop, Progress OpenEdge fires all applicable FIND triggers. FIND triggers fire only for records that completely satisfy the full search condition, such as a WHERE clause specifies. FIND triggers do not fire in response to the CAN–FIND function. If a FIND trigger fails, Progress OpenEdge behaves as though the record had not met the search criteria. If the FIND is within a FOR EACH block, Progress OpenEdge simply proceeds to the next record. If your application uses the BREAK option of the PRESELECT phrase (which forces Progress OpenEdge to retrieve two records at a time, so it can find the break), Progress OpenEdge executes the FIND trigger twice during the first FIND, which is actually two FINDs in succession. Thereafter, Progress OpenEdge looks one record ahead of the record currently in the record buffer, and executes the FIND trigger before it places the next record in the buffer.

WRITE - When Progress OpenEdge changes the contents of a record and validates it for a database table, Progress OpenEdge first fires all applicable WRITE triggers and then all applicable REPLICATION–WRITE triggers. Progress OpenEdge automatically validates a record when releasing it. You can also use the VALIDATE statement to explicitly validate a record. In either case, WRITE triggers execute before the validation occurs (so WRITE triggers can correct values and do more sophisticated validation). Progress might execute the WRITE triggers for a single record more than once before it writes the record to the database (if it validates the record more than once and you modify the record between validations).

ASSIGN - When Progress OpenEdge updates a field in a database record, Progress OpenEdge fires all applicable ASSIGN triggers. Unlike the other database events, this trigger monitors a specific field rather than a table. ASSIGN triggers execute when the contents of the associated field are modified. The trigger procedure executes at the end of a statement that assigns a new value to the field and after any necessary re-indexing. If the statement contains several field assignments (for example, UPDATE name city st), Progress OpenEdge fires each applicable ASSIGN trigger at the end of the statement. If any trigger fails, Progress OpenEdge undoes the statement (unless the code specifies NO–UNDO).

Schema and Session Database Triggers
Progress OpenEdge supports two types of triggers: schema and session. A schema trigger is a .p procedure that you add, through the Data Dictionary, to the schema of a database. A session trigger is a section of code that you add to a larger, enclosing procedure.

Differences Between Schema and Session Triggers
Although their syntax is slightly different, schema and session triggers provide similar functionality. The important difference between them is that schema triggers are independent procedures; whereas session triggers are contained within a larger procedure. Because of this difference, schema triggers always execute when a specified event occurs, regardless of what application initiates the event. Session triggers are defined as part of an application and are only in effect for that application.

Since session triggers are executed from within an enclosing procedure, they have access to the frames, widgets, and variables defined in the enclosing procedure. Since schema triggers are compiled separately from the procedure that initiates their execution, they do not have access to the procedure’s frames, widgets, and variables.

Use schema triggers for processing that you always want to perform for a specific event. For example, when an order record is deleted, you may always want to delete the corresponding order-line records. Use session triggers to perform additional or independent processing when the event occurs.

Both types of triggers can return ERRORS that cause the associated event to fail.

Describe Characteristics of Temporary Tables and Compare it with Work Tables


Temporary tables are database tables that Progress OpenEdge stores in a temporary database. You define temporary tables as you do work tables. Unlike work tables, temporary tables have indexes and perform at the speed of regular database tables. Unlike regular database tables, which are permanent and which multiple users can access simultaneously, temporary tables last only for the duration of the procedure that defines them (or for the duration of the Progress OpenEdge session, if you make them GLOBAL), and allow only one user at a time to access them. Finally, temporary tables are private, visible only to the user (process) that creates them. In short, if you want to sort, search, and process data for duration not longer than the Progress OpenEdge session, use temporary tables.

Progress OpenEdge stores temporary tables and temporary files in the same directory - by default, your current working directory. You can change this directory by using the Temporary Directory (–T) startup parameter.

Temporary tables require less memory than large work tables. The Buffers for Temporary Tables (–Bt) startup parameter allows you to set the size of the buffer that Progress OpenEdge uses to control temporary tables.

Differences Between Temporary and Work Tables
Temporary tables and work tables have the following differences:

When you define temporary tables with the LIKE option and do not explicitly specify indexes for them, they inherit index information from the database table. Work tables do not have index support.

While temporary table records are stored in a temporary database on disk, work table records are stored in memory. Note that for a relatively small application with a sufficiently large buffer allocation, Progress OpenEdge does not have to perform any disk I/O for temporary tables. To specify buffer size for temporary tables, use the –Bt parameter.

The performance of a temporary table is comparable to the performance of a database table. If you select the NO–UNDO option, UPDATE performance increases for temporary tables. If you do not use NO–UNDO, then changes made to the temporary table during a transaction are logged to the local before image (LBI) file.

The FIND statement for temporary tables uses the same cursor concept and choose-index logic that are used for regular database tables. When a temporary table record is created, its logical position in the table depends on the values of its key fields. Its logical position in the table can be changed by changing the key values.

Because temporary tables have indexes similar to regular database tables, they can perform fast sequential and random access to temporary table records using one or more indexes. Since work tables have no index support, all record access is performed with a sequential search.

If you use the Record phrase OF option to join a work table, the other table in the join (database or temporary) must have a unique index for the common fields.

You can pass temporary tables (but not work tables) as parameters to procedures. In fact, if you want to call a Progress OpenEdge AppServer passing it a table of data, you must pass the table of data as a temporary table (or set of temp-tables - prodataset).

Code snippet describing the usage of “OF” for table joins in progress OpenEdge


While Progress OpenEdge database does attempt to join tables using "OF", it does not implement the foreign key constraint automatically. Also, i am not comfortable using this technique. For the theory, here is a sample code snippet used on the sports2000 database.

/*
* Regular way when the table fields do not have same names across the database.
*/
for each customer
where customer.custnum = 2,
each order
where order.custnum = customer.custnum: /* joins the table */
display
order.custnum
order.ordernum.
end. /* for each customer */

/*
* Way when the table fields have same names across the database.
*/
for each customer
where customer.custnum = 2,
each order of customer: /* joins the table based on the same field names */
display
order.custnum
order.ordernum.
end. /* for each customer */

/* Sample of Table Relations as displayed for Customer Table in the option Data Dictionary or Data Administration > Reports > Table Relations */
/*
Customer:
Invoice OF Customer (CustNum)
RefCall OF Customer (CustNum)
ShipTo OF Customer (CustNum)
BillTo OF Customer (CustNum)
Order OF Customer (CustNum)
Customer OF Salesrep (SalesRep)
Customer OF State (State)
*/

Describe Data Movement in an OpenEdge application and demonstrate data movement for the ABL statement prompt-for.


 

Data Movement

Progress stores data in various locations—a database, a record buffer, a screen buffer, etc.
* A database stores application data on disk.
* A record buffer stores data temporarily while a procedure accesses the data, and stores the values of variables used in the procedure.
* A screen buffer stores data being displayed on the screen or being sent to another output destination; it also stores data that is being entered from the terminal or other input source.

Statements that move data from one location to another are called data-handling statements.

/* data movement demonstration  for prompt-for statement */
define variable a as char initial "abc" no-undo.
repeat:
/* code below user data input to the screen buffer */
prompt-for a.
/* code below demonstrates data in the screen buffer and the record buffer*/
message "Record buffer" a skip(0) "Screen buffer" input a
view-as alert-box.
/* code below moved data from screen buffer to record buffer */
assign a = input a.
/* code below demonstrates data in the screen buffer and the record buffer*/
message "Record buffer" a skip(0) "Screen buffer" input a
view-as alert-box.
end. /* repeat */

Demonstrate a browse program code using a temp-table.


/* browse.p - sports2000 database */

/* Definitions of the field level widgets */
DEFINE BUTTON btn_cancel AUTO-END-KEY LABEL "Cancel".

DEFINE BUTTON btn_ok AUTO-GO LABEL "OK".

DEFINE VARIABLE fi_employee AS INTEGER FORMAT "zzzzzzzzz9":U INITIAL 0
LABEL "Employee ID"
VIEW-AS FILL-IN
NO-UNDO.

/* ************************ Frame Definitions *********************** */

define frame frame-a
fi_employee AT ROW 1 COL 15 COLON-ALIGNED
btn_ok AT ROW 12 COL 4
btn_cancel AT ROW 12 COL 30
WITH KEEP-TAB-ORDER
SIDE-LABELS
NO-UNDERLINE SCROLLABLE
TITLE "Employee's Family"
width 150.

/*
* define temp-table for the browser
*/
def temp-table tt_family
field coveredonbenefits as log
field relativename as char
field relation as char
field benefitDate as date
field birthdate as date
field empnum AS int
index emp_key IS UNIQUE PRIMARY
empnum
relativename.

/*
* define query for the browser
*/
def query q_br_family for tt_family.

/*
* define browser
*/
def browse br_family
query q_br_family no-lock
display
tt_family.coveredonbenefits format "yes/no":U column-label "Covered":U
tt_family.relativename format "X(30)":U COLUMN-LABEL "Relative Name":T30
tt_family.relation format "X(30)":U COLUMN-LABEL "Relationship":T30
tt_family.benefitDate format "99/99/9999":U COLUMN-LABEL "Benefit Date":T30
tt_family.birthdate format "99/99/9999":U COLUMN-LABEL "Birth Date":T30
enable
tt_family.coveredonbenefits
with
5 down width 100
/* single */ multiple SEPARATORS.

/* event trigger on leave */
on leave of fi_employee
do:
assign fi_employee.
if fi_employee <> 0 then
do:
find employee
where employee.empnum = fi_employee
no-lock
no-error.
if not available employee then
do:
message "Not Found!":T80
view-as alert-box error buttons ok.
apply "ENTRY":U to fi_employee.
return no-apply.
end.
end.
run disp_emp_family.
end.

/* add browser to the frame */
def frame frame-a
br_family at row 3 col 5.

/* main code here */
enable all with frame frame-a. /* enables the widgets */
wait-for choose of btn_ok. /* waits for user input */
close query q_br_family.

procedure disp_emp_family :
/*------------------------------------------------------------------------------
Purpose:
Parameters:
Notes:
------------------------------------------------------------------------------*/
def var iv_list_items as char no-undo.
def var iv_rowid as rowid no-undo.

do with frame frame-a:
/* empty the temp-table */
for each tt_family:
delete tt_family.
end. /* for each tt_family */

for each family
where family.empnum = fi_employee
no-lock:
/* populate the data into the temp-table */
create tt_family.
buffer-copy family to tt_family.
release tt_family.
end. /* for each family */

/* initialize the query */
open query q_br_family
for each tt_family.
end.
end procedure.

Demonstrate passing temp-table as parameter.




/* 1 program - pass-tt.p */
/*
* TEMP-TABLE names are different in each procedure
* in the code sample, but it would also be correct to use
* the same name. This is also true for the field names.
* In all cases the table signatures (i.e., number of columns,
* data type of each column, index definitions, etc.)
* must be identical in both procedures.
* The following sample code shows how to pass a TEMP-TABLE
* between two external procedures.
* To pass a TEMP-TABLE as a parameter to an internal procedure,
* define the temp-table only once in the main procedure block.
* The parameter definition and parameter passing syntax remains
* the same as in this code sample:
*/

DEFINE TEMP-TABLE pass-table
FIELD pass_contact AS CHARACTER FORMAT "X(20)"
FIELD pass_country AS CHARACTER FORMAT "X(20)"
FIELD pass_balance AS DECIMAL
FIELD pass_discount AS INTEGER.

DO:
FOR EACH Customer WHERE CustNum < 30:
CREATE pass-table.
ASSIGN
pass_contact = customer.contact
pass_country = customer.country
pass_balance = customer.balance
pass_discount = customer.discount.
END.

RUN catch-tt.p (INPUT TABLE pass-table).
END.

/* 2 program - catch-tt.p */
DEFINE TEMP-TABLE catch-table
FIELD catch_contact AS CHARACTER FORMAT "X(20)"
FIELD catch_country AS CHARACTER FORMAT "X(20)"
FIELD catch_balance AS DECIMAL
FIELD catch_discount AS INTEGER FORMAT "-z9%".

DEFINE INPUT PARAMETER TABLE FOR catch-table.

FOR EACH catch-table NO-LOCK.
DISPLAY catch-table.
END.

Demonstrate passing a buffer as parameter to an external procedure.



/* program 1 - pass-buf.p */
define buffer buf_cust for customer.

find buf_cust
where custnum = 3 no-lock no-error.

RUN catch-buf.p (buffer buf_cust).

/* program 2 - catch-buf.p */
DEFINE PARAMETER BUFFER cust_buf FOR customer.

display cust_buf except comments with 2 col.


Demonstrate Editing and Trigger block functionality.


/* program 1 - editing.p Editing block - keystroke */
DEFINE FRAME cust-frame
Customer.Custnum SKIP Customer.name SKIP Customer.address SKIP
Customer.address2 SKIP Customer.city Customer.state SKIP
Customer.salesrep HELP "To see a list of values, press F6."
WITH DOWN SIDE-LABELS.
REPEAT WITH FRAME cust-frame:
PROMPT-FOR Customer.Custnum.
FIND Customer USING Custnum.
UPDATE Name Address Address2 City State Customer.Salesrep
EDITING:
READKEY.
IF FRAME-FIELD = "salesrep" AND LASTKEY = KEYCODE("F6")
THEN DO:
FOR EACH Salesrep:
DISPLAY Salesrep.Salesrep
WITH NO-LABELS 9 DOWN COLUMN 60 ROW 5.
END.
END.
ELSE DO:
APPLY LASTKEY.
END.
END.
END.

/* program 2 - trigger.p - Replaced with user interface triggers */
DEFINE FRAME cust-frame
Customer.Custnum SKIP Customer.name SKIP Customer.address SKIP
Customer.address2 SKIP Customer.city Customer.state SKIP
Customer.salesrep HELP "To see a list of values, press F6."
WITH DOWN SIDE-LABELS.
ON F6 OF Customer.Salesrep
DO:
FOR EACH salesrep:
DISPLAY Salesrep.Salesrep
WITH NO-LABELS 9 DOWN COLUMN 60 ROW 5.
END.
END.
REPEAT WITH FRAME cust-frame:
PROMPT-FOR Customer.Custnum.
FIND Customer USING Custnum.
UPDATE Name Address Address2 City State Salesrep.
END.

Describe the usage of NO-WAIT and LOCKED using a sample code.



/*run this at the same time in two sessions*/
/*
* The FIND statement in this procedure tries
* to retrieve a customer record according to a
* supplied customer number.
* Because of the NO-ERROR option, the FIND
* statement does not return an error if it cannot
* find the record. The NO-WAIT option causes
* FIND to return immediately if the record is in
* use by another user.
*/
repeat:
prompt-for customer.custnum.
find customer using customer.custnum
no-error
no-wait.
if not available customer then
do:
if locked (customer) then
message "Customer record is locked"
view-as
alert-box.
else
message "Customer record was not found"
view-as
alert-box.
next.
end. /* if not available customer then */
display
customer.custnum
customer.name
customer.city
customer.state.
end. /* repeat */