Summary:
See also: Connections, Transactions, Static SQL, Dynamic SQL, Result Sets, SQL Errors, Programs.
The BDL library "fgldbutl.4gl" provides several utility functions. For example, this library implements a function to get the type of the database engine at runtime. You will find this library in the FGLDIR/src directory. See the source file for more details.
In BDL, the DATABASE statement can be used in two distinct ways, depending on the context of the statement within its source module :
A default database is almost always used, because many BDL applications contain DEFINE ... LIKE statements. A problem occurs when the production database name differs from the development database name, because the default database specification will result in an automatic connection (just after MAIN):
01
DATABASE stock_dev02
DEFINE03
p_cust RECORD LIKE customer.*04
MAIN05
DEFINE dbname CHAR(30)06
LET dbname = "stock1"07
DATABASE dbname08
END MAIN
In order to avoid the implicit connection, you can use the SCHEMA instruction instead of DATABASE:
01
SCHEMA stock_dev02
DEFINE03
p_cust RECORD LIKE customer.*04
MAIN05
DEFINE dbname CHAR(30)06
LET dbname = "stock1"07
DATABASE dbname08
END MAIN
This instruction will define the database schema for compilation only, and will not make an implicit connection at runtime.
A BDL program can become very complex if a lot of nested functions do SQL processing. When using a database supporting transactions, you must sometimes execute all SQL statements in the same transaction block. This can be done easily by centralizing transaction control commands in wrapper functions.
The fgldbutl.4gl library contains special functions to manage the beginning and the end of a transaction with an internal counter, in order to implement nested function calls inside a unique transaction.
Example:
01
MAIN02
IF a() <> 0 THEN03
ERROR "..."04
END IF05
IF b() <> 0 THEN06
ERROR "..."07
END IF08
END MAIN09
10
FUNCTION a()11
DEFINE s INTEGER12
LET s = db_start_transaction( )13
UPDATE ...14
LET s = SQLCA.SQLCODE15
IF s = 0 THEN16
LET s = b( )17
END IF18
LET s = db_finish_transaction((s==0))19
RETURN s20
END FUNCTION21
22
FUNCTION b()23
DEFINE s INTEGER24
LET s = db_start_transaction( )25
UPDATE ...26
LET s = SQLCA.SQLCODE27
LET s = db_finish_transaction((s==0))28
RETURN s29
END FUNCTION
In this example, you see in the MAIN block that both functions a() and b() can be called separately. However, the transaction SQL commands will be used only if needed: When function a() is called, it starts the transaction, then calls b(), which does not start the transaction since it was already started by a(). When function b() is called directly, it starts the transaction.
The function db_finish_transaction()
is called with the expression (s==0)
,
which is evaluated before the call. This allows you to write in one line the
equivalent of the following IF statement:
01
IF s==0 THEN02
LET s = db_finish_transaction(1)03
ELSE04
LET s = db_finish_transaction(0)05
END IF
Beginning with Genero version 2.00, it is now possible to specify OUTPUT parameters to get values from stored procedures. While this new feature is generic, stored procedures execution needs to be addressed specifically according to the database type. There are different ways to execute a stored procedure. This section describes how to execute stored procedures on the supported database engines.
Tip: In order to write reusable code, you should encapsulate each stored procedure execution in a program function performing database-specific SQL based on a global database type variable. The program function would just take the input parameters and return the output parameters of the stored procedure, hiding database-specific execution steps from the caller.
With some database servers it is possible to execute stored procedures that produce a result set, and fetch the rows as normal SELECT statements, by using DECLARE, OPEN, FETCH. Some databases can return multiple result sets and cursor handles declared in a stored procedure as output parameters, but Genero supports only unique and anonymous result sets. See below for examples.
Informix distinguishes stored procedures from stored functions. Both must be written in the Informix stored procedure language called SPL.
There is no output parameter concept for typical SPL stored procedures or functions. If you want to return values from a database routine, you must use a stored function with a RETURNING clause. Informix stored procedures do not return values.
To execute a stored function with Informix, you must use the EXECUTE FUNCTION SQL instruction:
14
PREPARE stmt FROM "execute function proc1(?)"
In order to retrieve returning values into program variables, you must use an INTO clause in the EXECUTE instruction.
The following example shows how to call a stored function with Informix:
01
MAIN02
DEFINE n INTEGER03
DEFINE d DECIMAL(6,2)04
DEFINE c VARCHAR(200)05
DATABASE test106
EXECUTE IMMEDIATE "create function proc1( p1 integer )"07
|| " returning decimal(6,2), varchar(200);"08
|| " define p2 decimal(6,2);"09
|| " define p3 varchar(200);"10
|| " let p2 = p1 + 0.23;"11
|| " let p3 = 'Value = ' || p1;"12
|| " return p2, p3;"13
|| " end function;"14
PREPARE stmt FROM "execute function proc1(?)"15
LET n = 11116
EXECUTE stmt USING n INTO d, c17
DISPLAY d18
DISPLAY c19
END MAIN
Genero DB implements stored procedures as a group of statements that you can call by name. A subset of RDBMS-specific languages are supported by Genero DB; you can write Genero DB stored procedures in Informix SPL, Oracle PL/SQL or SQL Server Transact-SQL.
Genero DB stored procedures must be called with the input and output parameters specification in the USING clause of the EXECUTE, OPEN or FOREACH instruction. As in normal dynamic SQL, parameters must correspond by position, and the IN/OUT/INOUT options must match the parameter definition of the stored procedure.
To execute the stored procedure, you must use the CALL SQL instruction:
11
PREPARE stmt FROM "call proc1(?,?,?)"
Here is a complete example creating and calling a stored procedure:
01
MAIN02
DEFINE n INTEGER03
DEFINE d DECIMAL(6,2)04
DEFINE c VARCHAR(200)05
DATABASE test106
EXECUTE IMMEDIATE "create procedure proc1( p1 in int, p2 out number(6,2), p3 in out varchar2 )"07
|| " is begin"08
|| " p2 := p1 + 0.23;"09
|| " p3 := 'Value = ' || p1;"10
|| "end;"11
PREPARE stmt FROM "call proc1(?,?,?)"12
LET n = 11113
EXECUTE stmt USING n IN, d OUT, c INOUT14
DISPLAY d15
DISPLAY c16
END MAIN
With Genero DB, you can execute stored procedures returning a result set. To do so, you must declare a cursor and fetch the rows:
01
MAIN02
DEFINE i, n INTEGER03
DEFINE d DECIMAL(6,2)04
DEFINE c VARCHAR(200)05
DATABASE test106
CREATE TABLE tab1 ( c1 INTEGER, c2 DECIMAL(6,2), c3 VARCHAR(200) )07
INSERT INTO tab1 VALUES ( 1, 123.45, 'aaaaaa' )08
INSERT INTO tab1 VALUES ( 2, 123.66, 'bbbbbbbbb' )09
INSERT INTO tab1 VALUES ( 3, 444.77, 'cccccc' )10
EXECUTE IMMEDIATE "create procedure proc2 @key integer"11
|| " as begin"12
|| " select * from tab1 where c1 > @key"13
|| " end"14
DECLARE curs CURSOR FROM "call proc2(?)"15
LET i = 116
FOREACH curs USING i INTO n, d, c17
DISPLAY n, d, c18
END FOREACH19
END MAIN
It is possible to execute Genero DB stored procedures with output parameters and a result set. The output parameter values are available after the OPEN cursor instruction:
01
OPEN curs USING n IN, d OUT, c INOUT02
FETCH curs INTO rec.*
Oracle supports stored procedures and stored functions as a group of PL/SQL statements that you can call by name. Oracle stored functions are very similar to stored procedures, except that a function returns a value to the environment in which it is called. Functions can be used in SQL expressions.
Oracle stored procedures or stored functions must be called with the input and output parameters specification in the USING clause of the EXECUTE, OPEN or FOREACH instruction. As in normal dynamic SQL, parameters must correspond by position, and the IN/OUT/INOUT options must match the parameter definition of the stored procedure.
To execute the stored procedure, you must include the procedure in an anonymous PL/SQL block with BEGIN and END keywords:
11
PREPARE stmt FROM "begin proc1(?,?,?); end;"
Remark: Oracle stored procedures do not specify the size of number and character parameters. The size of output values (especially character strings) are defined by the calling context (i.e. the data type of the variable used when calling the procedure). When you pass a CHAR(10) to the procedure, the returning value will be filled with blanks to reach a size of 10 bytes.
Warning: For technical reasons, the Oracle driver uses dynamic binding with OCIBindDynamic(). The Oracle Call Interface does not support stored procedures parameters with the CHAR data type when using dynamic binding. You must use VARCHAR2 instead of CHAR to define character string parameters for stored procedures.
Here is a complete example creating and calling a stored procedure with output parameters:
01
MAIN02
DEFINE n INTEGER03
DEFINE d DECIMAL(6,2)04
DEFINE c VARCHAR(200)05
DATABASE test106
EXECUTE IMMEDIATE "create procedure proc1( p1 in int, p2 in out number, p3 in out varchar2 )"07
|| " is begin"08
|| " p2 := p1 + 0.23;"09
|| " p3 := 'Value = ' || to_char(p1);"10
|| "end;"11
PREPARE stmt FROM "begin proc1(?,?,?); end;"12
LET n = 11113
EXECUTE stmt USING n IN, d INOUT, c INOUT14
DISPLAY d15
DISPLAY c16
END MAIN
To execute the stored function returning a value, you must include the function in an anonymous PL/SQL block with BEGIN and END keywords, and use an assignment expression to specify the place holder for the returning value:
11
PREPARE stmt FROM "begin ? := func1(?,?,?); end;"
Oracle supports result set generation from stored procedures with the concept of cursor variables (REF CURSOR).
Warning: Genero does not support cursor references produced by Oracle stored procedures or functions.
IBM DB2 implements stored procedures as a saved collection of SQL statements, which can accept and return user-supplied parameters. IBM DB2 stored procedures can also produce one or more result sets. Beside stored procedures, IBM DB2 supports user defined functions, typically used to define scalar functions returning a simple value which can be part of SQL expressions.
IBM DB2 stored procedures must be called with the input and output parameters specification in the USING clause of the EXECUTE, OPEN or FOREACH instruction. As in normal dynamic SQL, parameters must correspond by position and the IN/OUT/INOUT options must match the parameter definition of the stored procedure.
To execute the stored procedure, you must use the CALL SQL instruction:
11
PREPARE stmt FROM "call proc1(?,?,?)"
Here is a complete example creating and calling a stored procedure with output parameters:
01
MAIN02
DEFINE n INTEGER03
DEFINE d DECIMAL(6,2)04
DEFINE c VARCHAR(200)05
DATABASE test106
EXECUTE IMMEDIATE "create procedure proc1( in p1 int, out p2 decimal(6,2), inout p3 varchar(20) )"07
|| " language sql begin"08
|| " set p2 = p1 + 0.23;"09
|| " set p3 = 'Value = ' || char(p1);"10
|| "end"11
PREPARE stmt FROM "call proc1(?,?,?)"12
LET n = 11113
EXECUTE stmt USING n IN, d OUT, c INOUT14
DISPLAY d15
DISPLAY c16
END MAIN
With DB2 UDB, you can execute stored procedures returning a result set. To do so, you must declare a cursor and fetch the rows:
01
MAIN02
DEFINE i, n INTEGER03
DEFINE d DECIMAL(6,2)04
DEFINE c VARCHAR(200)05
DATABASE test106
CREATE TABLE tab1 ( c1 INTEGER, c2 DECIMAL(6,2), c3 VARCHAR(200) )07
INSERT INTO tab1 VALUES ( 1, 123.45, 'aaaaaa' )08
INSERT INTO tab1 VALUES ( 2, 123.66, 'bbbbbbbbb' )09
INSERT INTO tab1 VALUES ( 3, 444.77, 'cccccc' )10
EXECUTE IMMEDIATE "create procedure proc2( in key integer )"11
|| " result sets 1"12
|| " language sql"13
|| " begin"14
|| " declare c1 cursor with return for"15
|| " select * from tab1 where c1 > key;"16
|| " open c1;"17
|| " end"18
DECLARE curs CURSOR FROM "call proc2(?)"19
LET i = 120
FOREACH curs USING i INTO n, d, c21
DISPLAY n, d, c22
END FOREACH23
END MAIN
It is possible to execute DB2 UDB stored procedures with output parameters and a result set. The output parameter values are available after the OPEN cursor instruction:
01
OPEN curs USING n IN, d OUT, c INOUT02
FETCH curs INTO rec.*
SQL Server implements stored procedures, which are a saved collection of Transact-SQL statements that can take and return user-supplied parameters. SQL Server stored procedures can also produce one or more result sets.
SQL Server stored procedures must be called with the input and output parameters specification in the USING clause of the EXECUTE, OPEN or FOREACH instruction. As in normal dynamic SQL, parameters must correspond by position and the IN/OUT/INOUT options must match the parameter definition of the stored procedure.
To execute the stored procedure, you must use an ODBC call escape sequence:
PREPARE stmt FROM "{ call proc1(?,?,?) }"
Here is a complete example creating and calling a stored procedure with output parameters:
01
MAIN02
DEFINE n INTEGER03
DEFINE d DECIMAL(6,2)04
DEFINE c VARCHAR(200)05
DATABASE test106
EXECUTE IMMEDIATE "create procedure proc1 @v1 integer, @v2 decimal(6,2) output, @v3 varchar(20) output"07
|| " as begin"08
|| " set @v2 = @v1 + 0.23"09
|| " set @v3 = 'Value = ' || cast(@v1 as varchar)"10
|| "end"11
PREPARE stmt FROM "{ call proc1(?,?,?) }"12
LET n = 11113
EXECUTE stmt USING n IN, d OUT, c OUT14
DISPLAY d15
DISPLAY c16
END MAIN
With SQL Server, you can execute stored procedures returning a result set. To do so, you must declare a cursor and fetch the rows.
Warning: The following example uses a stored procedure with a simple SELECT statement. If the stored procedure contains additional Transat-SQL statements such as SET or IF (which is the case in complex stored procedures), SQL Server generates multiple result sets. By default the Genero MSV driver uses "Server Cursors" to support multiple active SQL statements. But SQL Server stored procedures generating multiple result sets cannot be used with Server Cursors: The Server Cursor is silently converted to a "Default Result Set" cursor by the ODBC driver. Since Default Result Set cursors do not support multiple active statements, you cannot use another SQL statement while processing the results of such stored procedure. You must CLOSE the cursor created for the stored procedure before continuing with other SQL statements.
01
MAIN02
DEFINE i, n INTEGER03
DEFINE d DECIMAL(6,2)04
DEFINE c VARCHAR(200)05
DATABASE test106
CREATE TABLE tab1 ( c1 INTEGER, c2 DECIMAL(6,2), c3 VARCHAR(200) )07
INSERT INTO tab1 VALUES ( 1, 123.45, 'aaaaaa' )08
INSERT INTO tab1 VALUES ( 2, 123.66, 'bbbbbbbbb' )09
INSERT INTO tab1 VALUES ( 3, 444.77, 'cccccc' )10
EXECUTE IMMEDIATE "create procedure proc2 @key integer"11
|| " as select * from tab1 where c1 > @key"12
DECLARE curs CURSOR FROM "{ call proc2(?) }"13
LET i = 114
FOREACH curs USING i INTO n, d, c15
DISPLAY n, d, c16
END FOREACH17
END MAIN
SQL Server supports "Cursor Output Parameters": A stored procedure can declare/open a cursor and return a reference of the cursor to the caller.
Warning: SQL Server stored procedures returning a cursor as output parameter are not supported. There are two reasons for this: The Genero language does not have a data type to store a server cursor reference, and the underlying ODBC driver does not support this anyway.
SQL Server stored procedures can return integer values. To get the return value of a stored procedure, you must use an assignment expression in the ODBC call escape sequence:
01
PREPARE stmt FROM "{ ? = call proc3(?,?,?) }"
With SQL Server stored procedures, you call stored procedures with a return code, output parameters and producing a result set.
Warning: Return codes and output parameters are the last items returned to the application by SQL Server; they are not returned until the last row of the result set has been fetched, after the SQLMoreResults() ODBC function is called. If output parameters are used, the SQL Server driver executes a SQLMoreResult() call when closing the cursor instead of SQLCloseCursor(), to get the return code and output parameter values from SQL Server.
01
MAIN02
DEFINE r, i, n INTEGER03
DEFINE d DECIMAL(6,2)04
DEFINE c VARCHAR(200)05
DATABASE test106
CREATE TABLE tab1 ( c1 INTEGER, c2 DECIMAL(6,2), c3 VARCHAR(200) )07
INSERT INTO tab1 VALUES ( 1, 123.45, 'aaaaaa' )08
INSERT INTO tab1 VALUES ( 2, 123.66, 'bbbbbbbbb' )09
INSERT INTO tab1 VALUES ( 3, 444.77, 'cccccc' )10
EXECUTE IMMEDIATE "create procedure proc3 @key integer output"11
|| " as begin"12
|| " set @key = @key - 1"13
|| " select * from tab1 where c1 > @key"14
|| " return (@key * 3)"15
|| " end"16
DECLARE curs CURSOR FROM "{ ? = call proc3(?) }"17
LET i = 118
OPEN curs USING r INOUT, i INOUT19
DISPLAY r, i20
FETCH curs INTO n, d, c21
FETCH curs INTO n, d, c22
FETCH curs INTO n, d, c23
DISPLAY r, i24
CLOSE curs25
DISPLAY r, i -- Now the returned values are available26
END MAIN
Warning: Return code and output parameter variables must be defined as INOUT in the OPEN instruction.
PostgreSQL implements stored functions that can return values. If the function returns more that one value, you must specify the returning values as function parameters with the OUT keyword. If the function returns a unique value, you can use the RETURNS clause.
Warning: Pay attention to the function signature; PostgreSQL allows function overloading. For example, func(int) and func(char) are two different functions. To drop a function, you must specify the parameter type to identify the function signature properly.
To execute a stored function with PostgreSQL, you must use SELECT * FROM function, as shown in the next line:
14
PREPARE stmt FROM "select * from proc1(?)"
In order to retrieve returning values into program variables, you must use an INTO clause in the EXECUTE instruction.
The following example shows how to call a stored function with PostgreSQL:
01
MAIN02
DEFINE n INTEGER03
DEFINE d DECIMAL(6,2)04
DEFINE c VARCHAR(200)05
DATABASE test106
EXECUTE IMMEDIATE "create function proc1(p1 integer, out p2 numeric(6,2), out p3 varchar(200))"08
|| " as $$"09
|| " begin"10
|| " p2 := p1 + 0.23;"11
|| " p3 := 'Value = ' || cast(p1 as text);"12
|| " end;"13
|| " $$ language plpgsql"14
PREPARE stmt FROM "select * from proc1(?)"15
LET n = 11116
EXECUTE stmt USING n INTO d, c17
DISPLAY d18
DISPLAY c19
END MAIN
With PostgreSQL, you can execute stored procedures returning a result set. To do so, you must declare a cursor and fetch the rows:
01
MAIN02
DEFINE i, n INTEGER03
DEFINE d DECIMAL(6,2)04
DEFINE c VARCHAR(200)05
DATABASE test106
CREATE TABLE tab1 ( c1 INTEGER, c2 DECIMAL(6,2), c3 VARCHAR(200) )07
INSERT INTO tab1 VALUES ( 1, 123.45, 'aaaaaa' )08
INSERT INTO tab1 VALUES ( 2, 123.66, 'bbbbbbbbb' )09
INSERT INTO tab1 VALUES ( 3, 444.77, 'cccccc' )10
EXECUTE IMMEDIATE "create function proc2(integer)"11
|| " returns setof tab1"12
|| " as $$"13
|| " select * from tab1 where c1 > $1;"14
|| " $$ language sql"15
DECLARE curs CURSOR FROM "select * from proc2(?)"16
LET i = 117
FOREACH curs USING i INTO n, d, c18
DISPLAY n, d, c19
END FOREACH20
END MAIN
Warning: With PostgreSQL you cannot return output parameters and a result set from the same stored procedure; both use the same technique to return values to the client, in the context of result columns to be fetched.
MySQL implements stored procedures and stored functions as a collection of SQL statements that can take and return user-supplied parameters. Functions are very similar to procedures, except that they return a scalar value and can be used in SQL expressions.
Warning: Since MySQL C API (version 5.0) does not support an output parameter specification, the IN / OUT / INOUT technique cannot be used.
In order to return values from a MySQL stored procedure or stored function, you must use SQL variables. There are three steps to execute the procedure or function:
In order to retrieve returning values into program variables, you must use an INTO clause in the EXECUTE instruction.
The following example shows how to call a stored procedure with output parameters:
Warning: MySQL version 5.0 does not allow you to prepare the CREATE PROCEDURE statement; you may need to execute this statement from the mysql command line tool.
Warning: MySQL version 5.0 cannot execute "SELECT @variable" with server-side cursors. Since the Genero MySQL driver uses server-side cursors to support multiple active result sets, it is not possible to execute the SELECT statement to return output parameter values.
01
MAIN02
DEFINE n INTEGER03
DEFINE d DECIMAL(6,2)04
DEFINE c VARCHAR(200)05
DATABASE test106
EXECUTE IMMEDIATE "create procedure proc1(p1 integer, out p2 numeric(6,2), out p3 varchar(200))"07
|| " no sql begin"08
|| " set p2 = p1 + 0.23;"09
|| " set p3 = concat( 'Value = ', p1 );"10
|| " end;"11
LET n = 11112
EXECUTE IMMEDIATE "set @p1 = ", n13
EXECUTE IMMEDIATE "set @p2 = NULL"14
EXECUTE IMMEDIATE "set @p3 = NULL"15
EXECUTE IMMEDIATE "call proc1(@p1, @p2, @p3)"16
PREPARE stmt FROM "select @p2, @p3"17
EXECUTE stmt INTO d, c18
DISPLAY d19
DISPLAY c20
END MAIN
The following example shows how to retrieve the return value of a stored function with MySQL:
Warning: MySQL version 5.0 does not allow you to prepare the CREATE FUNCTION statement; you may need to execute this statement from the mysql command line tool.
01
MAIN02
DEFINE n INTEGER03
DEFINE c VARCHAR(200)04
DATABASE test105
EXECUTE IMMEDIATE "create function func1(p1 integer)"06
|| " no sql begin"07
|| " return concat( 'Value = ', p1 );"08
|| " end;"09
PREPARE stmt FROM "select func1(?)"10
LET n = 11111
EXECUTE stmt USING n INTO c12
DISPLAY c13
END MAIN
Warning: The MySQL version 5.0 stored procedures and stored functions cannot return a result set.
With Genero you can connect to several database sources from the same program by using the CONNECT instruction. When connected, you can DECLARE cursors or PREPARE statements, which can be used in parallel as long as you follow the rules. This section describes how to use SQL cursors and SQL statements in a multiple-connection program.
For convenience, the term Prepared SQL Statement and Declared Cursor will be grouped as SQL handle; from an internal point of view, both concepts merge into a unique SQL Handle, an object provided to manipulate SQL statements.
When you DECLARE a cursor or when you PREPARE a statement, you actually create an SQL Handle; the runtime system allocates resources for that SQL Handle before sending the SQL text to the database server via the database driver.
The SQL Handle is created in the context of the current connection, and must be used in that context, until it is freed or re-created with another DECLARE or PREPARE. If you try to use an SQL Handle in a different connection context than the one for which it was created, you will get a runtime error.
To change the current connection context, you must use the SET CONNECTION instruction. To set a specific connection, you must identify it by a name. To identify a connection, you typically use the AS clause of the CONNECT instruction. If you don't use the AS clause, the connection gets a default name based on the data source name. Since this might change as the database name changes, it is best to use an explicit name with the AS clause.
This small program example illustrates the use of two cursors with two different connections:
01
MAIN02
CONNECT TO "db1" AS "s1"03
CONNECT TO "db2" AS "s2"04
SET CONNECTION "s1"05
DECLARE c1 CURSOR FOR SELECT tab1.* FROM tab106
SET CONNECTION "s2"07
DECLARE c2 CURSOR FOR SELECT tab1.* FROM tab108
SET CONNECTION "s1"09
OPEN c110
SET CONNECTION "s2"11
OPEN c212
...13
END MAIN
The DECLARE and PREPARE instructions are a type of creator instructions; if an SQL Handle is re-created in a connection other than the original connection for which it was created, old resources are freed and new resources are allocated in the current connection context.
This allows you to re-execute the same cursor code in different connection contexts, as in the following example:
01
MAIN02
CONNECT TO "db1" AS "s1"03
CONNECT TO "db2" AS "s2"04
SET CONNECTION "s1"05
IF checkForOrders() > 0 ...06
SET CONNECTION "s2"05
IF checkForOrders() > 0 ...08
...09
END MAIN10
11
FUNCTION checkForOrders(d)12
DEFINE d DATE, i INTEGER13
DECLARE c1 CURSOR FOR SELECT COUNT(*) FROM orders WHERE ord_date = d14
OPEN c115
FETCH c1 INTO i16
CLOSE c117
FREE c118
RETURN i19
END FUNCTION
If the SQL handle was created in a different connection, the resources used in the old connection context are freed automatically, and new SQL Handle resources are allocated in the current connection context.
You can centralize SQL error identification in a BDL function:
01
CONSTANT SQLERR_FATAL = -102
CONSTANT SQLERR_LOCK = -203
CONSTANT SQLERR_CONN = -3
(constants must be defined in GLOBALS)
04
FUNCTION identifySqlError()05
CASE06
WHEN SQLCA.SQLCODE == -201 OR SQLCA.SQLERRD[2] == ...07
RETURN SQLERR_FATAL08
WHEN SQLCA.SQLCODE == -263 OR SQLCA.SQLERRD[2] == ...09
RETURN SQLERR_LOCK10
...11
END CASE12
END FUNCTION
The generic Informix error code is stored in SQLCA.SQLCODE register.
The native Database Provider error code is stored in SQLCA.SQLERRD[2] register.
If really needed, this would also allow adding a database specific test.
Although BDL allows you to write SQL statements directly in the program source as a part of the language (Static SQL), it is strongly recommended that you use Dynamic SQL instead when you are executing SQL statements within large program loops. Dynamic SQL allows you to PREPARE the SQL statements once and EXECUTE N times, improving performance.
To perform Static SQL statement execution, the database interface must use the basic API functions provided by the database vendor. These are usually equivalent to the PREPARE and EXECUTE instructions. So when you write a Static SQL statement in your BDL program, it is actually converted to a PREPARE + EXECUTE.
For example, the following BDL code:
01
FOR n=1 TO 10002
INSERT INTO tab VALUES ( n, c )03
END FOR
is actually equivalent to:
01
FOR n=1 TO 10002
PREPARE s FROM "INSERT INTO tab VALUES ( ?, ? )"03
EXECUTE s USING n, c04
END FOR
To improve the performance of the preceding code, use a PREPARE instruction before the loop and put an EXECUTE instruction inside the loop:
01
PREPARE s FROM "INSERT INTO tab VALUES ( ?, ? )"02
FOR n=1 TO 10003
EXECUTE s USING n, c04
END FOR
When you use an ANSI compliant RDBMS like Oracle or DB2, the database interface must perform a COMMIT after each statement execution. This generates unnecessary database operations and can slow down big loops. To avoid this implicit COMMIT, you can control the transaction with BEGIN WORK / COMMIT WORK around the code containing a lot of SQL statement execution.
For example, the following loop will generate 2000 basic SQL operations ( 1000 INSERTs plus 1000 COMMITs ):
01
PREPARE s FROM "INSERT INTO tab VALUES ( ?, ? )"01
FOR n=1 TO 10003
EXECUTE s USING n, c -- Generates implicit COMMIT04
END FOR
You can improve performance if you put a transaction block around the loop:
01
PREPARE s FROM "INSERT INTO tab VALUES ( ?, ? )"02
BEGIN WORK03
FOR n=1 TO 10004
EXECUTE s USING n, c -- In transaction -> no implicit COMMIT05
END FOR06
COMMIT WORK
With this code, only 1001 basic SQL operations will be executed ( 1000 INSERTs plus 1 COMMIT ).
However, you must take care when generating large transactions because all modifications are registered in transaction logs. This can result in a lack of database server resources (INFORMIX "transaction too long" error, for example) when the number of operations is very big. If the SQL operation does not require a unique transaction for database consistency reasons, you can split the operation into several transactions, as in the following example:
01
PREPARE s FROM "INSERT INTO tab VALUES ( ?, ? )"02
BEGIN WORK03
FOR n=1 TO 10004
IF n MOD 10 == 0 THEN05
COMMIT WORK06
BEGIN WORK07
END IF08
EXECUTE s USING n, c -- In transaction -> no implicit COMMIT09
END FOR10
COMMIT WORK
Some BDL applications do not care about long transactions because they use an Informix database without transaction logging (transactions are not stored in log files for potential rollbacks). However, if a failure occurs, no rollback can be made, and only some of the rows of a query might be updated. This could result in data inconsistency !
With many providers (Genero DB, SQL Server, IBM DB2, Oracle…), using transactions is mandatory. Every database modification is stored in a log file.
BDL applications must prevent long transactions when connected to a database using logging. If a table holds hundreds of thousands of rows, a "DELETE FROM table", for example, might cause problems. If the transaction log is full, no other insert, update or delete could be made on the database. The activity could be stopped until a backup or truncation of the log !
For example, if a table holds hundreds of thousands of rows, a "DELETE FROM table" might produce a "snapshot too old" error in ORACLE if the rollback segments are too small.
You must review the program logic in order to avoid long transactions:
In the end, increase the size of the transaction log to avoid it filling up.
Line 2 of the following example shows a cursor declared with a prepared statement:
01
PREPARE s FROM "SELECT * FROM table WHERE ", condition02
DECLARE c CURSOR FOR s
While this has no performance impact with Informix database drivers, it can become a bottleneck when using non-Informix databases:
Statement preparation consumes a lot of memory and processor resources. Declaring a cursor with a prepared statement is a native Informix feature, which does consume only one real statement preparation. But non-Informix databases do not support this feature. So the statement is prepared twice (once for the PREPARE, and once for the DECLARE). When used in a big loop, such code can cause performance problems.
To optimize such code, you can use the FROM clause in the DECLARE statement:
01
DECLARE c CURSOR FROM "SELECT * FROM table WHERE " || condition
By using this solution only one statement preparation will be done by the database server.
Remark: This performance problem does not appear with DECLARE statements using static SQL.
To write efficient SQL in a Genero program, you should use Dynamic SQL as described in 2.1 of this performance section. However, when using Dynamic SQL, you allocate an SQL statement handle on the client and server side, consuming resources. According to the database type, this can be a few bytes or a significant amount of memory. For example, on a Linux 32b platform, a prepared statement costs about 5 Kbytes with an Informix CSDK 2.80 client, while it costs about 20 Kbytes with an Oracle 10g client. That can be a lot of memory if you have programs declaring a dozen or more cursors, multiplied by hundreds of user processes. When executing several Static SQL statements, the same statement handle is reused and thus less memory is needed.
Genero allows you to use either Static or Dynamic SQL, so it's in your hands to choose memory or performance. However, in some cases the same code will be used by different kinds of programs, needing either low resource usage or good performance. In many OLTP applications you can actually distinguish two type of programs:
To reuse the same code for interactive programs and batch programs, you can do the following:
01
DEFINE up_prepared INTEGER02
03
FUNCTION getUserPermissions( username )04
DEFINE username VARCHAR(20)05
DEFINE cre, upd, del CHAR(1)06
07
IF NOT up_prepared THEN08
PREPARE up_stmt FROM "SELECT can_create, can_update, cab_delete"09
|| " FROM user_perms WHERE name = ?"10
LET up_prepared = TRUE11
END IF12
13
EXECUTE up_stmt USING username INTO cre, upd, del14
15
IF isInteractive() THEN16
FREE up_stmt17
LET up_prepared = FALSE18
END IF19
20
RETURN cre, upd, del21
22
END FUNCTION
The first time this function is called, the up_prepared
value will be FALSE,
so the statement will be prepared in line 08. The next time the function is
called, the statement will be re-prepared only if up_prepared
is TRUE.
The statement is executed in line 13 and values are fetch into the variables
returned in line 20. If the program is interactive, lines 15 to 18 free the
statement and set the up_prepared
module variable back to FALSE, forcing statement preparation in the next call of this function.
Writing portable SQL is mandatory if you want to succeed with different kind of database servers. This section gives you some hints to solve SQL incompatibility problems in your programs. Read this section carefully and review your program source code if needed. You should also read carefully the ODI Adaptation Guides which contain detailed information about SQL compatibility issues.
To easily detect SQL statements with specific syntax, you can use the -W stdsql option of fglcomp:
$ fglcomp -W stdsql orders.4gl module.4gl:15: SQL Statement or language instruction with specific SQL syntax.
Remark: This compiler option can only detect non-portable SQL syntax in Static SQL statements.
Most database servers can handle multiple database entities (you can create multiple 'databases'), but this is not possible with all engines:
Database Server Type | Multiple Database Entities |
GeneroDB | No |
IBM DB2 UDB (Unix) | Yes |
Informix | Yes |
Microsoft SQL Server | Yes |
MySQL | Yes |
Oracle Database Server | No |
PostgreSQL | Yes |
Sybase ASA | Yes |
When using a database server that does not support multiple database entities, you can emulate different databases with schemas, but this requires you to check for the database user definition. Each database user must have privileges to access any schema, and to see any table of any schema without needing to set a schema prefix before table names in SQL statements.
You can force the database driver to set a specific schema at connection with the following FGLPROFILE entry:
dbi.database.<dbname>.schema = "<schema-name>"
Some databases like GeneroDB also allow you to define a default schema for each database user. When the user connects to the database, the default schema is automatically selected.
To get the benefit of the database server security features, you should identify each physical user as a database user.
According to the type of server, you must do the following steps to create a database user:
Each database server has its specific users management and data access privilege mechanisms. Check the vendor documentation for security features and make sure you can define the users, groups, and privileges in all database servers you want to use.
Database creation statements like CREATE DATABASE, CREATE DBSPACE, DROP DATABASE cannot be executed with ODI database drivers. Such high-level database management statements are Informix-specific and require a connection to the server with no current database selected, which is not supported by the ODI architecture and drivers. However, for compatibility, the standard Informix drivers (with ix prefix) allow the BDL programs to execute such statements.
When using Data Definition Statements like CREATE TABLE, ALTER TABLE, DROP TABLE, only a limited SQL syntax works on all database servers. Most databases support NOT NULL, CHECK, PRIMARY KEY, UNIQUE, FOREIGN KEY constraints, but the syntax for naming constraints is different.
The following statement works with most database servers and creates a table with equivalent properties in all cases:
CREATE TABLE customer ( cust_id INTEGER NOT NULL, cust_name CHAR(50) NOT NULL, cust_lastorder DATE NOT NULL, cust_group INTEGER, PRIMARY KEY (cust_id), UNIQUE (cust_name), FOREIGN KEY (cust_group) REFERENCES group (group_id) )
Warning: Some engines like SQL Server have a different default behavior for NULL columns when you create a table. You may need to set up database properties to make sure that a column allows NULLs if the NOT NULL constraint is not specified.
When you want to create tables in programs using non-standard clauses (for example to define storage options), you must use Dynamic SQL and adapt the statement to the target database server.
The ANSI SQL specification defines standard data types, but for historical reasons most databases vendors have implemented native (non-standard) data types. You can usually use a synonym for ANSI types, but the database server always uses the native types behind the scenes. For example, when you create a table with an INTEGER column in Oracle, the native NUMBER data type is used. In your programs, avoid BDL data types that do not have a native equivalent in the target database. This includes simple types like floating point numbers, as well as complex data types like INTERVAL. Numbers may cause rounding or overflow problems, because the values stored in the database have different limits. For DECIMALs, always use the same precision and scale for the BDL variables and the database columns.
To write portable applications, we strongly recommend using the following BDL data types only:
BDL Data Type |
CHAR(n) |
VARCHAR(n) |
INTEGER |
SMALLINT |
DECIMAL(p,s) |
DATE |
DATETIME HOUR TO SECOND |
DATETIME YEAR TO FRACTION(n) |
See the ODI Adaptation Guides for more details about data type compatibility.
The CHAR and VARCHAR types are designed to store character strings, but all database servers do not have the same behavior when comparing two CHAR or VARCHAR values having trailing spaces.
With all kinds of databases servers, CHAR columns are always filled with blanks up to the size of the column, but trailing blanks are not significant in comparisons:
CHAR("abc ") = CHAR("abc")
With all database servers except Informix, trailing blanks are
significant when comparing VARCHAR values:
VARCHAR("abc ") <> VARCHAR("abc")
This is a major issue if you mix CHAR and VARCHAR columns and variables in your
SQL statements, because the result of an SQL query can be different depending on
whether you are using
Informix or another database server.
In BDL, CHAR variables are filled with blanks, even if the value used does not contain all spaces.
The following example:
01
DEFINE c CHAR(5)02
LET c = "abc"03
DISPLAY c || "."
shows the value "abc ."
(5 chars + dot).
In BDL, VARCHAR variables are assigned with the exact value specified, with significant trailing blanks.
For example, this code:
01
DEFINE v VARCHAR(5)02
LET v = "abc "03
DISPLAY v || "."
shows the value "abc ."
(4 chars + dot).
When comparing CHAR or VARCHAR variables in a BDL expression, the trailing blanks are not significant:
01
DEFINE c CHAR(5)02
DEFINE v1, v2 VARCHAR(5)03
LET c = "abc"04
LET v1 = "abc "05
LET v2 = "abc "06
IF c == v1 THEN DISPLAY "c==v1"07
END IF08
IF c == v2 THEN DISPLAY "c==v2"09
END IF10
IF v1 == v2 THEN DISPLAY "v1==v2"11
END IF
shows all three messages.
Additionally, when you assign a VARCHAR variable from a CHAR, the target variable gets the trailing blanks of the CHAR variable:
shows01
DEFINE pc CHAR(50)02
DEFINE pv VARCHAR(50)03
LET pc = "abc"04
LET pv = pc05
DISPLAY pv || "."
"abc <47 spaces>. "
( 50 chars + dot ).
To avoid this, you can use the CLIPPED operator:
LET pv = pc
CLIPPED
When you insert a row containing a CHAR variable into a CHAR or VARCHAR column,
the database interface removes the trailing blanks to avoid overflow problems,
(insert CHAR(100) into CHAR(20) when value is "abc"
must
work).
01
DEFINE c CHAR(5)02
LET c = "abc"03
CREATE TABLE t ( v1 CHAR(10), v2 VARCHAR(10) )04
INSERT INTO tab VALUES ( c, c )
The value in column v1 and v2 would be "abc"
( 3 chars in both columns ).
When you insert a row containing a VARCHAR variable into a VARCHAR column, the VARCHAR value in the database gets the trailing blanks as set in the variable. When the column is a CHAR(N), the database server fills the value with blanks so that the size of the string is N characters.
In the following example:DEFINE c VARCHAR(5) LET c = "abc " CREATE TABLE t ( v1 CHAR(10), v2 VARCHAR(10) ) INSERT INTO tab VALUES ( c, c )
The value in column v1 would be "abc
"
( 10 chars ) and v2 would be "abc "
(
5 chars ).
Use VARCHAR variables for VARCHAR columns, and CHAR variables for CHAR columns to achieve portability across all kinds of database servers.
See also: LENGTH() function.
Data Concurrency is the simultaneous access of the same data by many users. Data Consistency means that each user sees a consistent view of the database. Without adequate concurrency and consistency controls, data could be changed improperly, compromising data integrity. To write interoperable BDL applications, you must adapt the program logic to the behavior of the database server regarding concurrency and consistency management. This issue requires good knowledge of multi-user application programming, transactions, locking mechanisms, isolation levels and wait mode. If you are not familiar with these concepts, carefully read the documentation of each database server which covers this subject.
Processes accessing the database can change transaction parameters such as the isolation level. The main problem is to find a configuration which results in similar behavior on every database engine. Existing BDL programs must be adapted to work with this new behavior. ALL programs accessing the same database must be changed.
The following is the best configuration to get common behavior with all types of database engines :
Remarks: With this configuration, the locking granularity does not have to be at the row level. To improve performance with Informix databases, you can use the "LOCK MODE PAGE" locking level, which is the default.
SQLCA is the SQL Communication Area variable. SQLCA is a global record predefined by the runtime system, that can be queried to get SQL status information. After executing an SQL statement, members of this record contain execution or error data, but it is specific to Informix databases. For example, after inserting a row in a table with a SERIAL column, SQLCA.SQLERRD[2] contains the new generated serial number.
SQLCA.SQLCODE will be set to a specific Informix SQL error code, if the database driver can convert the native SQL error to an Informix SQL error. Additional members may be set, but that depends on the database server and database driver.
To identify SQL errors, you can also use SQLSTATE; this variable holds normalized ISO SQL error codes. However, even if Genero database drivers are prepared to support this register, not all RDBMS support standard SQLSTATE error codes:
Database Server Type | Supports SQLSTATE errors |
GeneroDB | No |
IBM DB2 UDB (Unix) | Yes, since version 7.1 |
Informix | Yes, since IDS 10 |
Microsoft SQL Server | Yes, since version 8 (2000) |
MySQL | Not in version 5.x |
Oracle Database Server | Not in version 10.2 |
PostgreSQL | Yes, since version 7.4 |
Sybase ASA | Not in version 8.x |
According to the above, SQL error identification requires quite complex code, which can be RDBMS-specific in some cases. Therefore, it is strongly recommended that you centralize SQL error identification in a function. This will allow you to write RDBMS-specific code, when needed, only once.
This section describes how to implement optimistic locking in BDL applications. Optimistic locking is a portable solution to control simultaneous modification of the same record by multiple users.
Traditional Informix-based applications use a SELECT FOR UPDATE to set a lock on the row to be edited by the user. This is called pessimistic locking. The SELECT FOR UPDATE is executed before the interactive part of the code, as described in here:
Note that if the Informix database was created with transaction logging, you must either start a transaction or define the SELECT FOR UPDATE cursor WITH HOLD option.
Unfortunately, this is not a portable solution. The lock wait mode should preferably be "WAIT" for portability reasons. Pessimistic locking is based on a "NOT WAIT" mode to return control to the program if a record is already locked by another process. Therefore, following the portable concurrency model, the pessimistic locking mechanisms must be replaced by the optimistic locking technique.
Basically, instead of locking the row before the user starts to modify the record data, the optimistic locking technique makes a copy of the current values(i.e. Before Modification Values), lets the user edit the record, and when it's time to write data into the database, checks if the BMVs still correspond to the current values in the database:
(rec1.*==rec2.*)
To compare 2 records, simply write:
01
IF new_record.* != bmv_record.* THEN02
LET values_have_changed = TRUE03
END IF
The optimistic locking technique could be implemented with a unique SQL instruction: an UPDATE could compare the column values to the BMVs directly (UPDATE ... WHERE kcol = kvar AND col1 = bmv.var1 AND ...). But, this is not possible when BMVs can be NULL. The database engine always evaluates conditional expressions such as "col=NULL" to FALSE. Therefore, you must use "col IS NULL" when the BMV is NULL. This means dynamic SQL statement generation based on the DMV values. Additionally, to use the same number of SQL parameters (? markers), you would have to use "col=?" when the BMV is not null and "col IS NULL and ? IS NULL" when the BMV is null. Unfortunately, the expression " ? IS [NOT] NULL " is not supported by all database servers (DB2 raises error SQL0418N).
If you are designing a new database application from scratch, you can also use the row versioning method. Each tables of the database must have a column that identifies the current version of the row. The column can be a simple INTEGER (to hold a row version number) or it can be a timestamp (DATETIME YEAR TO FRACTION(5) for example). To guaranty that the version or timestamp column is updated each time the row is updated, you should implement a trigger to increment the version or set the timestamp when an UPDATE statement is issued. If this is in place, you just need to check that the row version or timestamp has not changed since the user modifications started, instead of testing all field of the BMV record.
This section describes how to implement auto-incremented fields for portability.
INFORMIX provides the SERIAL data type which can be emulated by the database interface with most non-INFORMIX database engines. But, this requires additional configuration and maintenance tasks. If you plan to review the architecture of your programs, you should use this portable implementation instead of SERIALs emulated by the connectors when "ifxemul.serial" is true.
Principle:
In accordance with the target database, you must use the appropriate native serial generation method. Get the database type with the db_get_database_type() function of fgldbutl.4gl and use the appropriate SQL statements to insert rows with serial generation.
Warning : Not all database engines provide a sequence generator. Check the documentation of your target database.
Implementation:
BDL example:
01
DEFINE dbtype CHAR(3)02
DEFINE t1rec RECORD03
id INTEGER,04
name CHAR(50),05
cdate DATE06
END RECORD07
08
LET dbtype = db_get_database_type()09
10
IF dbtype = "IFX" THEN11
INSERT INTO t1 ( id, name, cdate )12
VALUES ( 0, t1rec.name, t1rec.cdate )13
LET t1rec.id = SQLCA.SQLERRD[2]14
END IF15
IF dbtype = "ORA" THEN16
INSERT INTO t1 ( id, name, cdate )17
VALUES ( t1seq.nextval, t1rec.name, t1rec.cdate )18
SELECT t1seq.currval INTO t1rec.id FROM dual19
END IF20
IF dbtype = "MSV" THEN21
INSERT INTO t1 ( name, cdate )22
VALUES ( t1rec.name, t1rec.cdate )23
PREPARE s FROM "SELECT convert(integer,@@identity)"24
EXECUTE s INTO t1rec.id25
END IF
Purpose:
The goal is to generate unique INTEGER numbers. These numbers will usually be used for primary keys.
Prerequisites:
Principle:
A dedicated table named "SEQREG" is used to register sequence numbers. The key is the name of the sequence. This name will usually be the table name the sequence is generated for. In short, this table contains a primary key that identifies the sequence and a column containing the last generated number.
The uniqueness is granted by the concurrency management of the database server. The first executed instruction is an UPDATE that sets an exclusive lock on the SEQREG record. When two processes try to get a sequence at the same time, one will wait for the other until its transaction is finished.
Implementation:
The "fgldbutl.4gl" utility library implements a function called "db_get_sequence" which generates a new sequence. You must create the SEQREG table as described in the fgldbutl.4gl source and make sure that every user has the privileges to access and modify this table.
BDL example:
01
DEFINE rec RECORD02
id INTEGER,03
name CHAR(100)04
END RECORD05
BEGIN WORK06
LET rec.id = db_get_sequence( "CUSTID" )07
IF rec.id>0 THEN08
INSERT INTO CUSTOMER ( CUSTID, CUSTNAME )09
VALUES ( rec.id, rec.name )10
ELSE11
ERROR "cannot get new sequence number"12
END13
COMMIT WORK
INFORMIX allows you to create databases in ANSI mode, which is supposed to be closer to ANSI standard behavior. Other databases like ORACLE and DB2 are 'ANSI' by default.
If you are not using the ANSI mode with Informix, we suggest you keep the database as is, because turning an Informix database into ANSI mode can result in unexpected behavior of the programs.
Here are some ANSI mode issues extracted from the Informix books:
It will take more time to adapt the programs to the INFORMIX ANSI mode than using the database interface to simulate the native mode of INFORMIX.
Informix supports WITH HOLD cursors using the FOR UPDATE clause. Such cursors can remain open across transactions (when using FOR UPDATE, locks are released at the end of a transaction, but the WITH HOLD cursor is not closed). This kind of cursor is Informix-specific and not portable. The SQL standards recommend closing FOR UPDATE cursors and release locks at the end of a transaction. Most database servers close FOR UPDATE cursors when a COMMIT WORK or ROLLBACK WORK is done. All database servers release locks when a transaction ends.
Database Server Type | WITH HOLD FOR UPDATE supported? |
GeneroDB | Yes (if primary key or unique index) |
IBM DB2 UDB (Unix) | No |
Informix | Yes |
Microsoft SQL Server | No |
MySQL | No |
Oracle Database Server | No |
PostgreSQL | No |
Sybase ASA | No |
It is mandatory to review code using WITH HOLD cursors with a SELECT statement having the FOR UPDATE clause.
The standard SQL solution is to declare a simple FOR UPDATE cursor outside the transaction and open the cursor inside the transaction:
01
DECLARE c1 CURSOR FOR SELECT ... FOR UPDATE02
BEGIN WORK03
OPEN c104
FETCH c1 INTO ...05
UPDATE ...06
COMMIT WORK
If you need to process a complete result set with many rows including updates of master and detail rows, you can declare a normal cursor and do a SELECT FOR UPDATE inside each transaction, as in the following example:
01
DECLARE c1 CURSOR FOR SELECT key FROM master ...02
DECLARE c2 CURSOR FOR SELECT * FROM master WHERE key=? FOR UPDATE03
FOREACH c1 INTO mrec.key04
BEGIN WORK05
OPEN c2 INTO mrec.key06
FETCH c2 INTO rec.*07
IF STATUS==NOTFOUND THEN08
ROLLBACK WORK09
CONTINUE FOREACH10
END IF11
UPDATE master SET ... WHERE CURRENT OF c212
UPDATE detail SET ... WHERE mkey=mrec.key13
COMMIT WORK14
END FOREACH
The "WHERE CURRENT OF cursor-name" clause in UPDATE and DELETE statements is not supported by all database engines.
Database Server Type | WHERE CURRENT OF supported? |
GeneroDB | Yes |
IBM DB2 UDB (Unix) | Yes |
Informix | Yes |
Microsoft SQL Server | Yes |
MySQL | Yes |
Oracle Database Server | No, emulated by driver with ROWIDs |
PostgreSQL | No, emulated by driver with OIDs |
Sybase ASA | Yes |
Some database drivers can emulate WHERE CURRENT OF mechanisms by using rowids, but this requires additional processing. You should review the code to disable this option.
The standard SQL solution is to use primary keys in all tables and write UPDATE / DELETE statements with a WHERE clause based on the primary key:
01
DEFINE rec RECORD02
id INTEGER,03
name CHAR(100)04
END RECORD05
BEGIN WORK06
SELECT CUSTID FROM CUSTOMER07
WHERE CUSTID=rec.id FOR UPDATE08
UPDATE CUSTOMER SET CUSTNAME = rec.name09
WHERE CUSTID = rec.id10
COMMIT WORK
Some database servers like INFORMIX allow single and double quoted string literals in SQL statements, both are equivalent:
SELECT COUNT(*) FROM table
WHERE col1 = "abc'def""ghi"
AND col1 = 'abc''def"ghi'
Most database servers do not support this specific feature:
Database Server Type | Double quoted string literals |
GeneroDB | No |
IBM DB2 UDB (Unix) | No |
Informix | Yes |
Microsoft SQL Server | Yes |
MySQL | No |
Oracle Database Server | No |
PostgreSQL | No |
Sybase ASA | No |
The ANSI SQL standards define doubles quotes as database object names delimiters, while single quotes are dedicated to string literals:
CREATE TABLE "my table" ( "column 1" CHAR(10) )
SELECT COUNT(*) FROM "my table" WHERE "column 1" = 'abc'
If you want to write a single quote character inside a string literal, you must write 2 single quotes:
... WHERE comment = 'John''s house'
When writing Static SQL in your programs, the double quoted string literals as converted to ANSI single quoted string literals by the fglcomp compiler. However, Dynamic SQL statements are not parsed by the compiler and therefore need to use single quoted string literals.
We recommend that you ALWAYS use single quotes for string literals and, if needed, double quotes for database object names.
INFORMIX allows you to specify date and time literals as a quoted character string in a specific format, depending upon DBDATE and GLS environment variables. For example, if DBDATE=DMY4, the following statement specifies a valid DATE literal:
SELECT COUNT(*) FROM table WHERE date_col = '24/12/2005'
Other database servers do support date/time literals as quoted character strings, but the date/time format specification is quite different. The parameter to specify the date/time format can be a database parameter, an environment variable, or a session option...
In order to write portable SQL, just use SQL parameters instead of literals:
01
DEFINE cnt INTEGER02
DEFINE adate DATE03
LET adate = '24/12/2005' -- DBDATE applies because this is BDL, not SQL!04
SELECT COUNT(*) INTO cnt FROM table05
WHERE date_col = adate
Or, when using dynamic SQL:
01
DEFINE cnt INTEGER02
DEFINE adate DATE03
LET adate = '24/12/2005'04
PREPARE s1 FROM "SELECT COUNT(*) FROM table WHERE date_col = ?"05
EXECUTE s1 USING adate INTO cnt
Each type of database server has its own naming conventions for database objects (i.e. tables and columns):
Database Server Type | Naming Syntax |
GeneroDB | [schema.]identifier |
IBM DB2 UDB (Unix) | [[database.]owner.]identifier |
Informix | [database[@dbservername]:][owner.]identifier |
Microsoft SQL Server | [[[server.][database].][owner_name].]object_name |
MySQL | [database.]identifier |
Oracle Database Server | [schema.]identifier[@database-link] |
PostgreSQL | [owner.]identifier |
Sybase ASA | [database.]identifier |
Most database engines have case-sensitive object identifiers. In most cases, when you do not specify identifiers in double quotes, the SQL parser automatically converts names to uppercase or lowercase, so that the identifiers match if the objects are also created without double quoted identifiers.
CREATE TABLE Customer ( cust_ID INTEGER )
In Oracle, the above statement would create a table named "CUSTOMER
"
with a "CUST_ID
" column.
The following table shows the case sensitivity features of each kind of database engine:
Database Server Type | Case sensitive names? | Not-quoted names converted to ... |
GeneroDB | Yes | Uppercase |
IBM DB2 UDB (Unix) | Yes | Uppercase |
Informix (1) | No | No |
Microsoft SQL Server (2) | Yes | Not converted |
MySQL | Yes | Not converted |
Oracle Database Server | Yes | Uppercase |
PostgreSQL | No | Lowercase |
Sybase ASA | No | Lowercase |
(1) If not ANSI database mode.
(2) Global parameter set at installation.
CREATE TABLE customer ( cust_id INTEGER ) -- first table CREATE TABLE Customer ( cust_id INTEGER ) -- second table
The maximum size of a table or column name depends on the database server type. Some database engines allow very large names (256c), while others support only short names (30c max). Therefore, using short names is required for writing portable SQL. Short names also simplify SQL programs.
We recommend that you use simple and short (<30c) database object names, without double quotes and without a schema/owner prefix:
CREATE TABLE customer ( cust_id INTEGER ) SELECT customer.cust_id FROM table
You may need to set the database schema after connection, so that the current database user can see the application tables without specifying the owner/schema prefix each time.
Tip: Even if all database engines do not required unique column names for all tables, we recommend that you define column names with a small table prefix (for example, CUST_ID in CUSTOMER table).
Not all database servers support temporary tables. The engines supporting this feature often provide it with a specific table creation statement:
Database Server Type | Temp table creation syntax | Local to SQL session? |
GeneroDB | CREATE TEMP TABLE tablename
( column-defs ) |
Yes |
IBM DB2 UDB (Unix) | DECLARE GLOBAL TEMPORARY
TABLE tablename ( column-defs ) |
Yes |
Informix | CREATE TEMP TABLE tablename
( column-defs ) |
Yes |
Microsoft SQL Server | CREATE TABLE #tablename ( column-defs
) |
Yes |
MySQL | CREATE TEMPORARY TABLE
tablename ( column-defs ) |
Yes |
Oracle Database Server | CREATE GLOBAL TEMPORARY
TABLE tablename ( column-defs ) |
No: only data is local to session |
PostgreSQL | CREATE TEMP TABLE tablename
( column-defs ) |
Yes |
Sybase ASA | CREATE GLOBAL TEMPORARY
TABLE tablename ( column-defs ) |
Yes |
Some databases even have a different behavior when using temporary tables. For example, Oracle 9i supports a kind of temporary table, but it must be created as a permanent table. The table is not specific to an SQL session: it is shared by all processes - only the data is local to a database session.
You must review the programs using temporary tables, and adapt the code to use database-specific temporary tables.
Old INFORMIX SQL outer joins specified with the OUTER keyword in the FROM part are not standard:
SELECT * FROM master, OUTER ( detail )
WHERE master.mid = detail.mid
AND master.cdate IS NOT NULL
Database Server Type | Supports Informix OUTER join syntax |
GeneroDB | Yes |
IBM DB2 UDB (Unix) | No (but translated by driver) |
Informix (1) | Yes |
Microsoft SQL Server (2) | No (but translated by driver) |
MySQL | No (but translated by driver) |
Oracle Database Server | No (but translated by driver) |
PostgreSQL | No (but translated by driver) |
Sybase ASA | No (but translated by driver) |
Most recent database servers now support the standard ANSI outer join specification:
SELECT * FROM master LEFT OUTER JOIN detail ON (master.mid = detail.mid)
WHERE master.cdate IS NOT NULL
You should use recent database servers and use ANSI outer joins only.
Only INFORMIX supports sub-string specification with square brackets:
SELECT * FROM item WHERE item_code[1,4] = "XBFG"
However, most database servers support a function that extracts sub-strings from a character string:
Database Server Type | Supports col[x,y] sub-strings? | Provides sub-string function? |
GeneroDB | Yes | SUBSTR(expr,start,length) |
IBM DB2 UDB (Unix) | No | SUBSTR(expr,start,length) |
Informix (1) | Yes | SUBSTR(expr,start,length) |
Microsoft SQL Server (2) | No | SUBSTRING(expr,start,length) |
MySQL | No | SUBSTR(expr,start,length) |
Oracle Database Server | No | SUBSTRING(expr,start,length) |
PostgreSQL | No | SUBSTRING(expr FROM
start FOR length ) |
Sybase ASA | No | SUBSTR(expr,start,length) |
Warning: INFORMIX allows you to update some parts
of a [VAR]CHAR column by using the sub-string specification ( UPDATE tab
SET col[1,2] ='ab'
). This is not possible with other databases.
Review the SQL statements using sub-string expressions and use the database specific sub-string function.
You could also create your own SUBSTRING() user function in all databases that do not support this function, to have a common way to extract sub-strings. In Microsoft SQL Server, when you create a user function, you must specify the owner as prefix when using the function. Therefore, you should create a SUBSTRING() user function instead of SUBSTR().
Rowids are implicit primary keys generated by the database engine. Not all database servers support rowids:
Database Server Type | Rowid keyword? | Rowid type? |
GeneroDB | ROWID |
INTEGER |
IBM DB2 UDB (Unix) | none |
none |
Informix (1) | ROWID |
INTEGER |
Microsoft SQL Server (2) | none |
none |
MySQL | none |
none |
Oracle Database Server | ROWID |
CHAR(18) |
PostgreSQL | OID |
internal type |
Sybase ASA | none |
none |
Warning: INFORMIX fills the SQLCA.SQLERRD[3] register with the ROWID of the last updated row. This register is an INTEGER and cannot be filled with rowids having CHAR(*) type.
Search for ROWID and SQLCA.SQLERRD[3] in your code and review the code to remove the usage of rowids.
The MATCHES operator allows you to scan a string expression:
SELECT * FROM customer WHERE customer_name MATCHES "A*[0-9]"
Here is a table listing the database servers which support the MATCHES operator:
Database Server Type | Support for MATCHES operator? |
GeneroDB | Yes |
IBM DB2 UDB (Unix) | No |
Informix (1) | Yes |
Microsoft SQL Server (2) | No |
MySQL | No |
Oracle Database Server | No |
PostgreSQL | No |
Sybase ASA | No |
The MATCHES operator is specific to INFORMIX SQL and Genero db. There is an equivalent
standard operator: LIKE. We recommend to replace MATCHES expressions in your SQL statements with a standard LIKE
expression. MATCHES uses *
and ?
as
wildcards. The equivalent wildcards in the LIKE operator are %
and _
.
Character ranges [a-z]
are not supported by the LIKE operator.
Remark: The BDL language provides a MATCHES operator which is part of the runtime system. Do not confuse this with the SQL MATCHES operator, used in SQL statements. There is no problem in using the MATCHES operator of the BDL language.
Warning: A program variable can be used as parameter for the MATCHES or LIKE operator, but you must pay attention to blank padding semantics of the target database. If the program variable is defined as a CHAR(N), it is filled by the runtime system with trailing blanks, in order to have a size of N. For example, when a CHAR(10) variable is assigned with "ABC%", it contains actually "ABC%<6 blanks>". If this variable is used as LIKE parameter, the database server will search for column values matching "ABC" + some characters + 6 blanks. To avoid automatic blanks, use a VARCHAR(N) data type instead of CHAR(N).
SELECT
a, b, sum(c) FROM table GROUP BY 1,2
This is not possible with all database servers:
Database Server Type | GROUP BY colindex, ... ? |
GeneroDB | No |
IBM DB2 UDB (Unix) | No |
Informix (1) | Yes |
Microsoft SQL Server (2) | No |
MySQL | Yes |
Oracle Database Server | No |
PostgreSQL | Yes |
Sybase ASA | No |
Search for GROUP BY in your SQL statements and use explicit column names.
Not all database servers support the LENGTH() function, and some have specific behavior:
Database Server Type | Length function? | Counts trailing blanks for CHAR() columns? | Return value when NULL |
GeneroDB | LENGTH(expr) | No | NULL |
IBM DB2 UDB (Unix) | LENGTH(expr) | Yes | NULL |
Informix (1) | LENGTH(expr) | No | NULL |
Microsoft SQL Server (2) | LEN(expr) | No | NULL |
MySQL | LENGTH(expr) | No | NULL |
Oracle Database Server | LENGTH(expr) | Yes | NULL |
PostgreSQL | LENGTH(expr) | Yes | NULL |
Sybase ASA | LENGTH(expr) | No | NULL |
Search for LENGTH in your SQL statements and review the code of the database-specific function. You could also define your own LEN() user function to have a common function in all databases. In Microsoft SQL Server, when you create a user function, you must specify the owner as prefix when using the function. Therefore, you should create a LEN() user function instead of LENGTH().
Remark: The BDL language provides a LENGTH built-in function which is part of the runtime system. Do not confuse this with the SQL LENGTH() function, used in SQL statements. There is no problem in using the LENGTH() function of the BDL language. However, the LENGTH() function of the language returns zero when the string expression is NULL.
With Informix, it is possible to interrupt a long-running query if the SQL INTERRUPT ON option is set by the Genero program. The database server returns SQLCODE -213, which can be trapped to detect a user interruption.
01
MAIN02
DEFINE n INTEGER03
DEFER INTERRUPT04
OPTIONS SQL INTERRUPT ON05
DATABASE test106
WHENEVER ERROR CONTINUE07
-- Start long query (self join takes time)08
-- From now on, user can hit CTRL-C in TUI mode to stop the query09
SELECT COUNT(*) INTO n FROM customers a, customers b10
WHERE a.cust_id <> b.cust_id11
IF SQLCA.SQLCODE == -213 THEN12
DISPLAY "Statement was interrupted by user..."13
EXIT PROGRAM 114
END IF15
WHENEVER ERROR STOP16 ... 17
END MAIN
When SQL Interruption is available for a database server type, Genero database drivers implement it to behave as in Informix, converting the native error to the code -213. Not all database servers support SQL interruption:
Database Server Type | SQL Interruption API | SQL error code for interrupted query |
GeneroDB 3.80 | SQLCancel() | Native error -30005 |
IBM DB2 UDB 9.x | SQLCancel() | Native error -952 |
Informix | sqlbreak() | Native error -213 |
Microsoft SQL Server 2005 (SNC driver only) | SQLCancel() | SQLSTATE HY008 |
MySQL | N/A | ? |
Oracle Database Server 8.x, 9.x, 10.x | OCIBreak() | Native error -1013 |
PostgreSQL 8.x | PQCancel() | SQLSTATE 57014 |
Sybase ASA | N/A | ? |