Back to Summary


Tutorial Chapter 4: Query by Example

Summary:


This program  implements query-by-example, using the CONSTRUCT statement to allow the user to enter search criteria in a form. The criteria is used to build an SQL SELECT statement which will retrieve rows from the customer database table.  A SCROLL CURSOR is defined in the program, to allow the user to scroll back and forth between the rows of the result set.  The SQLCA.SQLCODE is used to test the success of the SQL statements.  Handling errors, and allowing the user to cancel the query, is illustrated.

                                 Display on Windows platforms

Implementing Query-by-Example

Query-by-Example allows users to enter a value or a range of values for one or several form fields. Then your program looks up the database rows that satisfy the requirements. The BDL statement that makes this possible is CONSTRUCT.  

Steps:

  1. Define fields linked to database columns in a form specification file.
  2. Define a STRING variable in your program to hold the query criteria.
  3. Open a window and display the form.
  4. Activate the form with the interactive dialog statement CONSTRUCT, for entry of the query criteria. Control is turned over to the user to enter his criteria.
  5. The user enters his criteria in the fields specified in the CONSTRUCT statement.  The CONSTRUCT statement accepts logical operators in any of the fields to indicate ranges, comparisons, sets, and partial matches. Using the form in this program, for example, the user can enter a specific value, such as IL in the state field, to retrieve all the rows from the customer table where the state column = IL.  Or he can enter relational tests, such as > 103, in the Store # field, to retrieve only those rows where the store_num column is greater than 103.
  6. After entering his criteria, the user selects OK, to instruct your program to continue with the query, or Cancel to terminate the dialog.
    In this program, the action views for accept (OK) and cancel are displayed as buttons on the screen.
  7. If the user accepts the dialog, the CONSTRUCT statement creates a Boolean expression by generating a logical expression for each field with a value and then applying unions (and relations) to the field statements.  This expression is stored in the character string that you specified in the CONSTRUCT statement.
  8. You can then use the Boolean expression to create a STRING variable containing a complete SELECT statement.  You must supply the WHERE keyword to convert the Boolean expression into a WHERE clause. Make sure that you supply the spaces required to separate the constructed Boolean expression from the other parts of the SELECT statement.
  9. Execute the statement to retrieve the row(s) from the database table, after preparing it or declaring a cursor for SELECT statements that might retrieve more than one row.

Using CONSTRUCT and STRING variables

A basic CONSTRUCT statement has the following format:

     CONSTRUCT <variable-name> ON <column-list> FROM <field-list

This statement temporarily binds the specified form fields to database columns. It allows you to identify database columns for which the user can enter search criteria.  Each field and CONSTRUCT corresponding column must be the same or compatible data types.  You can use the BY NAME clause when the fields on the screen form have the same names as the corresponding columns in the ON clause. The user can query only the screen fields implied in the BY NAME clause.

     CONSTRUCT BY NAME <variable-name> ON <column-list>

The runtime system converts the entered criteria into a Boolean SQL condition that can appear in the WHERE clause of a SELECT statement. The variable to hold the query condition can be defined as a STRING  data type. Strings are a variable length, dynamically allocated character string data type, without a size limitation. The STRING variable can be concatenated, using the double pipe operator (||), with the text required to form a complete SQL SELECT statement. The LET statement can be used to assign a value to the variable. For example:

     DEFINE where_clause, sqltext STRING
     CONSTRUCT BY NAME where_clause ON customer.*
     LET sql_text = "SELECT COUNT(*) FROM customer WHERE " || where_clause
   

                          Display on Windows Platform

In this example the user has entered the criteria > 101 in the store_num field.  The where_clause would be generated as 

     "store_num > 101"

and the complete sql_text would be 

     "SELECT COUNT(*) FROM customer WHERE store_num > 101"

Preparing the SQL Statement 

The STRING created in the example is not valid for execution.  The PREPARE instruction sends the text of the string to the database server for parsing, validation, and to generate the execution plan.  The scope of a prepared SQL statement is the module in which it is declared.

     PREPARE cust_cnt_stmt FROM sql_text

A prepared SQL statement can be executed with the EXECUTE instruction. 

     EXECUTE cust_cnt_stmt INTO cust_cnt

Since the SQL statement will only return one row (containing the count) the INTO syntax of the EXECUTE instruction can be used to store the count in the local variable cust_cnt. (The function cust_select  illustrates the use of database cursors with SQL SELECT statements.)

When a prepared statement is no longer needed, the FREE instruction will release the resources associated with the statement.

     FREE cust_cnt_stmt

Allowing the User to Cancel the Query Operation

Predefined Actions (accept/cancel)

The language pre-defines some actions and associated names for common operations, such as accept or cancel, used during interactive dialogs with the user such as CONSTRUCT.  You do not have to define these actions in the interactive instruction block, the runtime system interprets predefined actions. For example, when the accept action is caught, the dialog is validated.

You can define action views (such as buttons, toolbar icons, menu items) in your form using these pre-defined names; the corresponding action will automatically be attached to the view.  If you do not define any action views for the actions, default buttons (such as OK/Cancel) for these actions will be displayed on the form as appropriate when interactive dialog statements are executed.

When the CONSTRUCT statement executes, buttons representing accept and cancel actions will be displayed by default, allowing the user to validate or cancel the interactive dialog statement. If the user selects Cancel, the INT_FLAG is automatically set to TRUE. Once INT_FLAG is set to TRUE, your program must re-set it to FALSE to detect a new cancellation. You typically set INT_FLAG to FALSE before you start a dialog instruction, and you test it just after (or in the AFTER CONSTRUCT / AFTER INPUT block) to detect if the dialog was canceled:

     LET INT_FLAG = FALSE
     CONSTRUCT BY NAME where_part
        ...
     END CONSTRUCT
     IF INT_FLAG = TRUE THEN
        ...
     END IF

DEFER INTERRUPT and the INT_FLAG

The statement DEFER INTERRUPT in your MAIN program block will prevent your program from terminating abruptly if a SIGINT signal is received. When using a GUI interface, the user can generate an interrupt signal if you have an action view named 'interrupt' (the predefined interrupt action).  If an interrupt event is received, TRUE is assigned to the built-in global integer variable INT_FLAG.

It is up to the programmer to manage the interruption event (stop or continue with the program), by testing the value of INT_FLAG variable.

Interruption handling is discussed in the report example, in chapter 9.

Conditional Logic

Once the CONSTRUCT statement is completed, you must test whether the INT_FLAG was set to TRUE (whether the user cancelled the dialog). Genero BDL provides the conditional logic statements IF or CASE to test a set of conditions.

The IF statement

     IF <conditionTHEN
       ....
     ELSE
       ....
     END IF

IF statements can be nested. The ELSE clause may be omitted.

If condition is TRUE, the runtime system executes the block of statements following THEN, until it reaches either the ELSE keyword or the END IF keywords. Your program resumes execution after END IF. If condition is FALSE, the runtime system executes the block of statements between ELSE and END IF.

 IF (INT_FLAG = TRUE) THEN
       LET INT_FLAG = FALSE
       LET cont_ok = FALSE
     ELSE
       LET cont_ok = TRUE
     END IF

The CASE statement

 The CASE statement specifies statement blocks to be executed conditionally, depending on the value of an expression. Unlike IF statements, CASE does not restrict the logical flow of control to only two branches.  Particularly if you have a series of  nested IF statements, the CASE statement may be more readable. In the previous example, the CASE statement could have been substituted for the IF statement:

     CASE
     WHEN (INT_FLAG = TRUE)
       LET INT_FLAG = FALSE
       LET cont_ok = FALSE     
     OTHERWISE
       LET cont_ok = TRUE 
     END CASE

Usually, there would be several conditions to check.  The following statement uses an alternative syntax, since all the conditions check the value of var1:

     CASE var1
     WHEN 100
       CALL routine_100()
     WHEN 200
       CALL routine_200()
     OTHERWISE
       CALL error_routine() 
     END CASE

The first WHEN condition in the CASE statement will be evaluated.  If the condition is true(var1=100), the statement block is executed and the CASE statement is exited. If the condition is not true, the next WHEN condition will be evaluated, and so on through subsequent WHEN statements until a condition is found to be true, or OTHERWISE or END CASE is encountered.  The OTHERWISE clause of the CASE statement can be used as a catch-all for unanticipated cases.

See Flow Control for other examples of IF and CASE syntax and the additional conditional statement WHILE.


The Query program

The Query program consists of two modules.  The custmain.4gl module must be linked with the custquery.4gl module in order for the program to be run.  The line numbers shown in the code are for reference only, and are not a part of the code.


Example: Module custmain.4gl

This module contains the MAIN program block for the query program, and the MENU that drives the query actions. 

Module custmain.4gl
01 MAIN
02   
03   DEFER INTERRUPT
04
05   CONNECT TO "custdemo"
06   CLOSE WINDOW SCREEN
07   OPEN WINDOW w1 WITH FORM "custform"
08
09   MENU "Customer"
10     ON ACTION query
11       CALL query_cust() 
12     ON ACTION next           
13        CALL fetch_rel_cust(1)
14     ON ACTION previous
15        CALL fetch_rel_cust(-1)
16     ON ACTION exit
17       EXIT MENU
18   END MENU
19
20   CLOSE WINDOW w1
21 
22   DISCONNECT CURRENT
23
24 END MAIN

Notes:

There are no further statements so the Query program terminates.


Example: Module custquery.4gl

This module of the Query program contains the logic for querying the database and displaying the data retrieved.  The function query_cust is called by the "query" option of the MENU in custmain.4gl.

Module custquery.4gl (and function query_cust)
01 -- custquery.4gl
02
03 SCHEMA custdemo
04
05 DEFINE  mr_custrec RECORD
06  store_num    LIKE customer.store_num,
07  store_name   LIKE customer.store_name,
08  addr         LIKE customer.addr,
09  addr2        LIKE customer.addr2,
10  city         LIKE customer.city,
11  state        LIKE customer.state,
12  zipcode      LIKE customer.zipcode,
13  contact_name LIKE customer.contact_name,
14  phone          LIKE customer.phone
15 END RECORD
16
17 FUNCTION query_cust()
18   DEFINE cont_ok      SMALLINT,
19         cust_cnt     SMALLINT,
20         where_clause STRING
21   MESSAGE "Enter search criteria"
22   LET cont_ok = FALSE
23
24   LET INT_FLAG = FALSE
25   CONSTRUCT BY NAME where_clause
26      ON customer.store_num,
27         customer.store_name,
28         customer.city,
29         customer.state,
30         customer.zipcode,
31         customer.contact_name,
32         customer.phone
33
34   IF (INT_FLAG = TRUE) THEN
35     LET INT_FLAG = FALSE
36     CLEAR FORM
37     LET cont_ok = FALSE
38     MESSAGE "Canceled by user."
39   ELSE      
40     CALL get_cust_cnt(where_clause)
41       RETURNING cust_cnt
42     IF (cust_cnt > 0) THEN
43       MESSAGE cust_cnt USING "<<<<", 
44                  " rows found."
45        CALL cust_select(where_clause)
46          RETURNING cont_ok
47     ELSE
48       MESSAGE "No rows found."
49       LET cont_ok = FALSE    
50     END IF
51   END IF
52
53   IF (cont_ok = TRUE) THEN
54     CALL display_cust()
55   END IF
56
57 END FUNCTION

Notes:


Example: custquery.4gl (Function get_cust_cnt) 

This function is called by the function query_cust to return the count of rows that would be retrieved by the SELECT statement.  The criteria previously entered by the user and stored in the variable where_clause is used.

  Function get_cust_cnt
01 FUNCTION get_cust_cnt(p_where_clause)
02   DEFINE p_where_clause STRING,
03         sql_text STRING,
04         cust_cnt SMALLINT
05
06   LET sql_text = 
07    "SELECT COUNT(*) FROM customer" || 
08    " WHERE " || p_where_clause
09
10   PREPARE cust_cnt_stmt FROM sql_text
11   EXECUTE cust_cnt_stmt INTO cust_cnt
12   FREE cust_cnt_stmt
13
14   RETURN cust_cnt
15
16 END FUNCTION

Notes:


Retrieving data from the Database

Using Cursors

When an SQL SELECT statement in your application will retrieve more than one row, a cursor must be used to pass the selected data to the program one row at a time.  The cursor is a data structure that represents a specific location within the active set of rows that the SELECT statement retrieved.

The scope of a cursor is the module in which it is declared. Cursor names must be unique within a module.

The general sequence of  program statements when using a SELECT cursor for Query-by-Example is:

The cursor program statements must appear physically within the module in the order listed.


The SQLCA.SQLCODE

The "SQLCA" name stands for "SQL Communication Area".  The SQLCA variable is a predefined record containing information on the execution of an SQL statement. The  SQLCA record is filled after any SQL statement execution.  The SQLCODE member of this record contains the SQL execution code:

Execution Code Description
0 SQL statement executed successfully.
100 No rows were found.
<0 An SQL error occurred.

The NOTFOUND constant is a predefined integer value that evaluates to 100. This constant is typically used to test the execution status of an SQL statement returning a result set, to check if rows have been found.


Example custquery.4gl (function cust_select)

This function is called by the function query_cust, if the row count returned by the function get_cust_cnt indicates that the criteria previously entered by the user and stored in the variable where_clause would produce an SQL SELECT result set.

  Function cust_select
01 FUNCTION cust_select(p_where_clause) 
02   DEFINE p_where_clause STRING,
03         sql_text STRING,
04         fetch_ok SMALLINT
05
06   LET sql_text = "SELECT store_num, " ||
07    " store_name, addr, addr2, city, " ||
08    " state, zipcode, contact_name, phone " ||
09    " FROM customer WHERE " || p_where_clause ||
10    " ORDER BY store_num"
11
12   DECLARE cust_curs SCROLL CURSOR FROM sql_text
13   OPEN cust_curs
14   CALL fetch_cust(1)   -- fetch the first row 
15      RETURNING fetch_ok
16   IF NOT (fetch_ok) THEN
17      MESSAGE "no rows in table."   
18   END IF
19
20   RETURN fetch_ok
21   
22 END FUNCTION

Notes:


Example: custquery.4gl (function fetch_cust)

This function is designed so that it can be re-used each time a row is to be fetched from the customer database table; a variable is passed to indicate whether the cursor should move forward one row or backward one row.

  Function fetch_cust
01 FUNCTION fetch_cust(p_fetch_flag)
02    DEFINE p_fetch_flag SMALLINT,
03           fetch_ok SMALLINT
04
05   LET fetch_ok = FALSE
06   IF (p_fetch_flag = 1) THEN
07     FETCH NEXT cust_curs
08       INTO mr_custrec.*
09   ELSE
10     FETCH PREVIOUS cust_curs
11       INTO mr_custrec.*
12   END IF
13
14   IF (SQLCA.SQLCODE = NOTFOUND) THEN
15     LET fetch_ok = FALSE
16   ELSE
17     LET fetch_ok = TRUE
18   END IF
19
20   RETURN fetch_ok
21
22 END FUNCTION

Notes:


Example: custquery.4gl (function fetch_rel_cust)

This function is called by the MENU options "next" and "previous" in the custmain.4gl module.

Function fetch_rel_cust
01 FUNCTION fetch_rel_cust(p_fetch_flag)
02   DEFINE p_fetch_flag SMALLINT,
03         fetch_ok SMALLINT
04
05   MESSAGE " "       
06   CALL fetch_cust(p_fetch_flag)
07     RETURNING fetch_ok
08
09   IF (fetch_ok) THEN
10     CALL display_cust()
11   ELSE
12     IF (p_fetch_flag = 1) THEN
13       MESSAGE "End of list"
14     ELSE
15       MESSAGE "Beginning of list"
16     END IF
17   END IF
18
19 END FUNCTION

Notes:


Example: custquery.4gl (function display_cust)

This function displays the contents of the mr_custrec record in the form.  It is called by the functions query_cust and fetch_rel_cust.

Function display_cust
01 FUNCTION display_cust()
02   DISPLAY BY NAME mr_custrec.*
03 END FUNCTION

Notes:


Compiling and Linking the Program

The two example modules must be compiled and  then linked into a single program.

From the command line:

	fglcomp custmain.4gl 
	fglcomp custquery.4gl 

This produces the object modules custmain.42m and custquery.42m, which must be linked to produce the program cust.42r:

	fgllink -o cust.42r custmain.42m custquery.42m

Or, compile both modules and link at the same time:

	fgl2p -o cust.42r custmain.4gl custquery.4gl

Modifying the Program to Handle Errors

The WHENEVER ERROR statement

Since program statements that access the database may be expected to fail occasionally (the row is locked, etc.) the WHENEVER ERROR statement can be used to handle this type of error.

By default, when a runtime error occurs the program will stop.  To prevent this happening when SQL statements that access the database fail, surround the SQL statement with WHENEVER ERROR statements, as in the following example based on the fetch_cust function in the custquery.4gl program module:

 01 IF (p_fetch_flag = 1) THEN
 02    WHENEVER ERROR CONTINUE
 03    FETCH NEXT cust_curs
 04       INTO mr_custrec.*
 05    WHENEVER ERROR STOP
 06 ...

WHENEVER ERROR statements are modular in scope, and generate additional code for exception handling when the module is compiled. This exception handling is valid until the end of the module or until a new WHENEVER ERROR instruction is encountered by the compiler.

When the example code is compiled, WHENEVER ERROR CONTINUE will generate code to prevent the program from stopping if the FETCH statement fails. Immediately after the FETCH statement, the WHENEVER ERROR STOP instruction will generate the code to re-set the default behavior for the rest of the module.

You can write your own error function to handle SQL errors, and use the WHENEVER ERROR CALL <function-name> syntax to activate it.  Run-time errors may be logged to an error log.

Negative SQLCA.SQLCODE

The database server returns an execution code whenever an SQL statement  is executed, available in SQLCA.SQLCODE.  If the code is a negative number,  an SQL error has occurred.  Just as we checked the SQLCA.SQLCODE for the NOTFOUND condition, we can also check the code for database errors (negative SQLCODE).  The SQLCA.SQLCODE should be checked immediately after each SQL statement that may fail, including  DECLARE, OPEN, FETCH, etc. For simplicity of the examples, the error handling in these programs is minimal.

SQLERRMESSAGE

If an SQL error occurs, the SQLERRMESSAGE operator returns the error message associated with the error code.  This is a character string that can be displayed to the user with the ERROR instruction.

     ERROR SQLERRMESSAGE 
Changes to function fetch_cust (custquery.4gl)
01 FUNCTION fetch_cust (p_fetch_flag)
02    DEFINE p_fetch_flag SMALLINT,
03           fetch_ok     SMALLINT
04
05   LET fetch_ok = FALSE
06   IF (p_fetch_flag = 1) THEN
07      WHENEVER ERROR CONTINUE
08     FETCH NEXT cust_curs
09       INTO mr_custrec.*
10     WHENEVER ERROR STOP
11   ELSE
12      WHENEVER ERROR CONTINUE
13     FETCH PREVIOUS cust_curs
14       INTO mr_custrec.*
15     WHENEVER ERROR STOP
16   END IF
17
18   CASE
19   WHEN (SQLCA.SQLCODE = 0)
20      LET fetch_ok = TRUE
21   WHEN (SQLCA.SQLCODE = NOTFOUND)
22      LET fetch_ok = FALSE
23   WHEN (SQLCA.SQLCODE < 0)
24      LET fetch_ok = FALSE
25     ERROR SQLERRMESSAGE
26   END CASE 
27
28   RETURN fetch_ok
29
30 END FUNCTION

Notes:

Close and Free the Cursor

Closing and freeing the cursor when you no longer need it is good practice, especially if the modules are part of a larger program. This function must be placed in the same module as the DECLARE/OPEN/FETCH statements and in sequence, so this is the last function in the query_cust module.  However, the function can be called from cust_main, as a final "cleanup" routine.  

Function cleanup (custquery.4gl)
01 FUNCTION cleanup() 
02   WHENEVER ERROR CONTINUE 
03   CLOSE cust_curs 
04   FREE cust_curs 
05   WHENEVER ERROR STOP
06 END FUNCTION

Notes:


Error if Cursor is not Open

In the example program in this chapter, if the user selects the Next or Previous action from the MENU before he has queried, the program returns an error ("Program stopped at line .... Fetch attempted on unopened cursor."). One way to prevent this error would be to add a variable to the program to indicate whether the user has queried for a result set, and to prevent him from executing the actions associated with Next or Previous until he has done so.

Changes to function query_cust (custquery.4gl)
01 FUNCTION query_cust()
02   DEFINE cont_ok      SMALLINT,
03         cust_cnt     SMALLINT,
04         where_clause STRING
05   MESSAGE "Enter search criteria"
06   LET cont_ok = FALSE
07
...
08
09   IF (cont_ok = TRUE) THEN
10     CALL display_cust()
11   END IF
12    
13   RETURN cont_ok 
14
15 END FUNCTION

Notes:

Changes to module custmain.4gl
01 MAIN
02   DEFINE query_ok SMALLINT
03 
04   DEFER INTERRUPT
05
06   CONNECT TO "custdemo"
07   CLOSE WINDOW SCREEN
08   OPEN WINDOW w1 WITH FORM "custform"
09   LET query_ok = FALSE
10
11   MENU "Customer"
12     ON ACTION query
13       CALL query_cust() RETURNING query_ok
14     ON ACTION next           
15       IF (query_ok) THEN
16         CALL fetch_rel_cust(1)
17       ELSE
18         MESSAGE "You must query first."
19       END IF
20     ON ACTION previous
21       IF (query_ok) THEN
22         CALL fetch_rel_cust(-1)
23       ELSE
24         MESSAGE "You must query first."
25       END IF
26     ON ACTION quit
27       EXIT MENU
28   END MENU
29
30   CLOSE WINDOW w1
31   CALL cleanup()
32   DISCONNECT CURRENT
33
34 END MAIN

Notes: