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
With Genero BDL you can 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.
IBM Informix stored procedures are written in the SPL, C or Java programming
languages, also known as User Defined Routines.
See IBM Informix IDS documentation for more details.
The traditional way to return values from an IBM Informix SPL routine is to fetch the output values, as from a regular SELECT producing a result set.
You must define a stored function with a RETURNING clause. IBM Informix stored procedures do not return values.
To execute a stored function with IBM 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 IBM 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
Starting with IDS 10.00, IBM Informix introduced the concept of output parameters for stored procedures, like other database vendors.
To retrieve the output parameters, you must execute the routine in a SELECT
statement defining Statement Locale Variables. These variables will be
listed in the SELECT clause to be fetched as regular column values.
See IBM Informix documentation for more details.
In order to retrieve returning values into program variables, you must use an INTO clause in the EXECUTE instruction.
The next example shows how to call a stored procedure with output parameters in IBM Informix:
01
MAIN02
DEFINE pi, pr INT03
DATABASE test104
EXECUTE IMMEDIATE "create function proc2(i INT, OUT r INT)"05
||" returning int;"06
||" let r=i+10;"07
||" return 1;"08
||" end function"09
PREPARE s FROM "select r from systables where tabid=1 and proc1(?,r#int)==1"10
LET pi = 3311
EXECUTE s USING pi INTO pr12
DISPLAY "Output value: ", pr13
EXECUTE IMMEDIATE "drop function proc2"14
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 IBM 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.
Note that 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).
Note that 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.
The next 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
Note that it is possible to fetch large objects (text/image) from stored procedure generating a result set, however, if the stored procedure executes other statements as the SELECT (like SET/IF commands), the SQL Server ODBC driver will convert the server cursor to a regular default result set cursor, requiring the LOB columns to appear at the end of the SELECT list. Thus, in most cases (stored procedures typically use SET / IF statements), you will have to move the LOB columns and the end of the column list.
SQL Server supports "Cursor Output Parameters": A stored procedure can declare/open a cursor and return a reference of the cursor to the caller.
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 you can call stored procedures with a return code, output parameters and producing a result set.
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 SQLMoreResults() 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
Note that 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.
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
Note that 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.
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:
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.
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.
MySQL version >=5.0 evaluates "@variable" user variables assigned with a string as large text (CLOB) expressions. That type of values must normally be fetched into TEXT variable in BDL. To workaround this behavior, you can use the substring(@var,1,255) function to return a VARCHAR() expression from MySQL and fetch into a VARCHAR() BDL variable.
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, substring(@p3,1,200)"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:
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
Note that MySQL version 5.0 stored procedures and stored functions cannot return a result set.
Sybase ASE supports stored procedures, which can take and return user-supplied parameters. Sybase ASE stored procedures can also produce one or more result sets.
Sybase ASE 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 a specific syntax to have the ODI driver identify the statement as an RPC call. The syntax of an RPC call must be:
!rpc procedure-name ( [ @param-name [,...] ] )
The parameter names must be specified, with the same names as the arguments of the stored procedure, because the ODI driver must bind stored procedure parameters by name.
Example:
PREPARE stmt FROM "!rpc update_account ( @custid, @old, @new )"
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 "!rpc proc1( @v1, @v2, @v3 )"12
LET n = 11113
EXECUTE stmt USING n IN, d OUT, c OUT14
DISPLAY d15
DISPLAY c16
END MAIN
With Sybase, you can execute stored procedures returning a result set. To do so, you must declare a cursor and fetch the rows.
When the stored procedure generates 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 "!rpc proc2( @key )"13
LET i = 114
FOREACH curs USING i INTO n, d, c15
DISPLAY n, d, c16
END FOREACH17
END MAIN
With Sybase ASE stored procedures, you call stored procedures with a return code, output parameters and producing a result set.
Return codes and output parameters are the last items returned to the application by Sybase; they are not returned until the last row of the result set has been fetched.
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 "!rpc proc3( @key ) }"17
LET i = 118
OPEN curs USING r OUT, i OUT19
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
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.SQLCODE == -244 OR SQLCA.SQLERRD[2] == ...09
RETURN SQLERR_LOCK10
...11
END CASE12
END FUNCTION
If really needed, this would also allow adding a database specific test.
See also: SQL error registers.
Generally, when using scrollable cursors, the database server or the database client software (i.e. the application) will make a static copy of the result set produced by the SELECT statement. For example, when using an IBM Informix database engine, each scrollable cursor will create a temporary table to hold the result set. Thus, if the SELECT statement returns all columns of the table(s) in the FROM clause, the database software will make a copy of all these values. This practice has two disadvantages: A lot of resources are consumed, and the data is static.
A good programming pattern to save resources and always get fresh data from the database server is to declare two cursors based on the primary key usage, if the underlying database table has a primary key (or unique index constraint): The first cursor must be a scrollable cursor that executes the SELECT statement, but returns only the primary keys. The SELECT statement of this first cursor is typically assembled at runtime with the where-part produced by a CONSTRUCT interactive instruction, to give a sub-set of the rows stored in the database. The second cursor (actually, a PREPARE/EXECUTE statement handle) performs a single-row SELECT statement listing all columns to be fetched for a given record, based on the primary key value of the current row in the scrollable cursor list. The second statement must use a ? question mark place holder to execute the single-row SELECT with the current primary key as SQL parameter.
If the primary key SELECT statement needs to be ordered, check that the database engine allows that columns used in the ORDER BY clause do not need to appear in the SELECT list. For example, this was the case with IBM Informix servers prior to version 9.4. If needed, the SELECT list can be completed with the columns used in ORDER BY, you can then just list the variable that holds the primary key in the INTO clause of FETCH.
Note also that the primary key result set is static. That is, if new rows are inserted in the database or if rows referenced by the scroll cursor are deleted after the scroll cursor was opened, the result set will be out-dated. In this case, you can refresh the primary key result set by re-executing the scroll cursor with CLOSE/OPEN commands.
Next code example illustrates this programming pattern:
01
MAIN02
DEFINE wp VARCHAR(500)03
DATABASE test104
-- OPEN FORM / DISPLAY FORM with c_id and c_name fields05
...06
-- CONSTRUCT generates wp string...07
...08
LET wp = "c_name LIKE 'J%'"09
DECLARE clist SCROLL CURSOR FROM "SELECT c_id FROM customer WHERE " || wp10
PREPARE crec FROM "SELECT * FROM customer WHERE c_id = ?"11
OPEN clist12
MENU "Test"13
COMMAND "First" CALL disp_cust("F")14
COMMAND "Next" CALL disp_cust("N")15
COMMAND "Previous" CALL disp_cust("P")16
COMMAND "Last" CALL disp_cust("L")17
COMMAND "Refresh" CLOSE clist OPEN clist18
COMMAND "Quit" EXIT MENU19
END MENU20
FREE crec21
FREE clist22
23
END MAIN24
25
FUNCTION disp_cust(m)26
DEFINE m CHAR(1)27
DEFINE rec RECORD28
c_id INTEGER,29
c_name VARCHAR(50)30
END RECORD31
CASE m32
WHEN "F" FETCH FIRST clist INTO rec.c_id33
WHEN "N" FETCH NEXT clist INTO rec.c_id34
WHEN "P" FETCH PREVIOUS clist INTO rec.c_id35
WHEN "L" FETCH LAST clist INTO rec.c_id36
END CASE37
INITIALIZE rec.* TO NULL38
IF SQLCA.SQLCODE == NOTFOUND THEN39
ERROR "You reached to top or bottom of the result set."40
ELSE41
EXECUTE crec USING rec.c_id INTO rec.*42
IF SQLCA.SQLCODE == NOTFOUND THEN43
ERROR "Row was not found in the database, refresh the result set."44
END IF45
END IF46
DISPLAY BY NAME rec.*47
END FUNCTION
When connecting to a database server, the user must be identified by the server. Once connected, the current user is authenticated and identified by the db server, and the database system can then apply specific privileges, audit user activity, and so on.
DB user authentication is typically achieved by specifying a login and password in the CONNECT instruction. However, most database servers support additional user authentication methods, such as OS user authentication, trusted connections, LDAP authentication, Single Sign-on authentication and even specific pluggable authentication methods.
Follow the simple security patterns described below to avoid basic user authentication problems:
SQL Injection is a well-known attack that started to appear with Web applications, where the end user enters SQL statement fragments in form fields that are normally designed to hold simple data. When the entered text is used to complete a SQL statement without further checking, there is a risk of SQL statements being injected by the user to intentionally harm the database.
To illustrate the problem, see the following code:
01
MAIN02
DEFINE sql CHAR(200), cn CHAR(50), n INTEGER03
OPEN FORM f FROM "custform"04
DISPLAY FORM f05
INPUT BY NAME cn06
LET sql = "SELECT COUNT(*) FROM customers WHERE custname = '", cn, "'"07
PREPARE stmt FROM sql08
EXECUTE stmt INTO n08
DISPLAY "Count = ", n10
END MAIN
If the end user enters for example:
[xxx' ; delete from customers ]
The resulting SQL statement will contain an additional DELETE command that will drop all rows of the customers table:
SELECT COUNT(*) FROM customers WHERE custname = 'xxx'; delete from customers
To avoid SQL injection attacks, do not build SQL instructions dynamically by concatenating user input that is not checked. Instead of basic concatenation, use static SQL statements with program variables (if dynamic SQL is not needed), use parameterized queries (with ? parameter placeholders), or use the CONSTRUCT instruction to implement a Query By Example form.
Simple static SQL example:
01
MAIN02
DEFINE cn CHAR(50), n INTEGER03
OPEN FORM f FROM "custform"04
DISPLAY FORM f05
INPUT BY NAME cn06
SELECT COUNT(*) INTO n FROM customers WHERE custname = cn07
DISPLAY "Count = ", n08
END MAIN
Parameterized query example:
01
MAIN02
DEFINE sql CHAR(200), cn CHAR(50), n INTEGER03
OPEN FORM f FROM "custform"04
DISPLAY FORM f05
INPUT BY NAME cn06
LET sql = "SELECT COUNT(*) FROM customers WHERE custname = ?"07
PREPARE stmt FROM sql08
EXECUTE stmt USING cn INTO n09
DISPLAY "Count = ", n10
END MAIN
CONSTRUCT example:
01
MAIN02
DEFINE sql CHAR(200), cond CHAR(50), n INTEGER03
OPEN FORM f FROM "custform"04
DISPLAY FORM f05
CONSTRUCT BY NAME cond ON custname06
LET sql = "SELECT COUNT(*) FROM customers WHERE ", cond07
PREPARE stmt FROM sql08
EXECUTE stmt INTO n09
DISPLAY "Count = ", n10
END MAIN
Although Genero BDL allows you to write SQL statements directly in the program source as a part of the language (Static SQL), it is sometimes more efficient to use Dynamic SQL instead, when you are executing SQL statements repeatedly. Dynamic SQL allows you to PREPARE the SQL statements once and EXECUTE N times, improving performance.
Note however that implementing prepared statements with Dynamic SQL has a cost in terms of database resources and code readability: When a simple Static SQL statement is executed, database client and server resources are allocated for the statement and are reused for the next Static SQL statement. With Dynamic SQL, you define a statement handle and allocate database resources that last until you FREE the handle. Regarding code readability, Static SQL statements can be written directly in the source code (as another BDL statement), while Dynamic SQL uses several instructions and takes the SQL text as a string expression. Additionally, Static SQL statements are parsed at compile time so you can detect syntax errors in the SQL text before executing the programs.
Therefore, Dynamic SQL should only be used if the SQL statement is created at runtime (Ex: CONSTRUCT) or if the execution time is too long with Static SQL (assuming that it's only a statement preparation issue).
To perform Static SQL statement execution, the database interface must use the basic API functions provided by the database client. 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
To mimic the IBM Informix auto-commit behavior with an ANSI compliant RDBMS like Oracle or DB2, the database interface must perform an implicit COMMIT after each statement execution, if the SQL statement is not inside a transaction block. 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 IBM 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 filling it 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 IBM Informix database drivers, it can become a bottleneck when using non-IBM Informix databases:
Statement preparation consumes a lot of memory and processor resources. Declaring a cursor with a prepared statement is a native IBM Informix feature, which consumes only one real statement preparation. Non-IBM 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, this code can cause performance problems.
To optimize the code, 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.
Note: This performance problem does not occur 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 IBM 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 BOOLEAN02
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 fetched 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 |
Genero db | No |
IBM DB2 UDB (Unix) | Yes |
IBM Informix | Yes |
Microsoft SQL Server | Yes |
MySQL | Yes |
Oracle Database Server | No |
PostgreSQL | Yes |
Sybase ASE | 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 Genero db 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 IBM 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 IBM 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) )
Note that 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.
Genero BDL supports several SQL syntaxes for the INSERT, UPDATE and DELETE statements. Some of the syntaxes are IBM Informix specific, but will be converted to standard SQL by the compiler.
The following statements are standard SQL and work with all database servers:
(1) INSERT INTO table (column-list) VALUES (value-list) (2) UPDATE table SET column = value, ... [WHERE condition] (3) DELETE FROM table [WHERE condition]
The next statements are not standard SQL, but are converted by the compiler to standard SQL, working with all database servers:
(4) INSERT INTO table VALUES record.* -- where record is defined LIKE a table from db schema (5) UPDATE table SET (column-list) = (value-list) [WHERE condition] (6) UPDATE table SET {[table.]*|(column-list)} = record.* ... [WHERE condition] -- where record is defined LIKE a table from db schema
The next statement is not standard SQL and will not be converted by the compiler, so it must be reviewed:
(7) UPDATE table SET [table.]* = (value-list) [WHERE condition]
You can easily search for non-portable SQL statements in your .4gl sources by compiling with the -Wstdall fglcomp option.
For maximum SQL portability, INSERT statements should be reviewed to avoid the SERIAL column from the value list.
For example, the following statement:
INSERT INTO tab (col1,col2) VALUES (0, p_value)
should be converted to :
INSERT INTO tab (col2) VALUES (p_value)
Static SQL INSERT using records defined from the schema file should also be reviewed:
DEFINE rec LIKE tab.* INSERT INTO tab VALUES ( rec.* ) -- will use the serial column
should be converted to :
INSERT INTO tab VALUES rec.* -- without braces, serial column is removed
For more details about supported SQL DML statements, see Static SQL.
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.
When defining a CHAR/VARCHAR database column or BDL variable, you must specify a size. When using a multi-byte character set, it is important to understand what unit is used for that size: it can be specified in bytes or characters. For BDL, the size of CHAR/VARCHAR variables is specified in bytes. For SQL/databases, the meaning of the size depends from the type of database and its configuration.
For more details, see Localization.
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 IBM 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 IBM 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).
Note that assigning an empty string to a CHAR or VARCHAR variable will set the variable to NULL:
01
DEFINE v VARCHAR(5)02
LET v = ""03
IF v IS NULL THEN04
DISPLAY "is null" -- will be displayed05
END IF
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 THEN07
DISPLAY "c==v1"08
END IF09
IF c == v2 THEN10
DISPLAY "c==v2"11
END IF12
IF v1 == v2 THEN13
DISPLAY "v1==v2"14
END IF
All three messages are shown.
Additionally, when you assign a VARCHAR variable from a CHAR, the target variable gets the trailing blanks of the CHAR variable:
01
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 ) is shown.
To avoid this, 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:01
DEFINE vc VARCHAR(5)02
LET vc = "abc " -- note 2 spaces at end of string03
CREATE TABLE t ( v1 CHAR(10), v2 VARCHAR(10) )04
INSERT INTO tab VALUES ( vc, vc )
The value in column v1 would be "abc
"
( 10 chars ) and v2 would be "abc "
(
5 chars ).
At the SQL level, most databases distinguish ''
empty strings
from NULL (with some exceptions like Oracle DB). However, for the Genero BDL
language, an empty string is the equivalent to NULL in program variables. As
result, it is not possible to distinguish an empty string from a NULL when such
values are fetched from the database. Note that this limitation is only visible
when fetching VARCHAR columns and expressions fetched into VARCHAR variables,
because CHAR columns get filled with blanks if the database returns a CHAR
column value that was filled with an empty string; CHAR columns get blanks up to
the max size.
Use VARCHAR variables for VARCHAR columns, and CHAR variables for CHAR columns to achieve portability across all kinds of database servers.
Avoid storing empty strings in VARCHAR columns, or make sure that your program is prepared to get NULLs while the database stores empty strings.
When possible, use Byte Length Semantics in the database to define CHAR/VARCHAR columns, to fit the length semantics of Genero BDL.
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 IBM 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 IBM 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 IBM Informix SQL error code, if the database driver can convert the native SQL error to an IBM 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 |
Genero db | Not in version 3.61 |
IBM DB2 UDB (Unix) | Yes, since version 7.1 |
IBM Informix | Yes, since IDS 10 |
Microsoft SQL Server | Yes, since version 8 (2000) |
MySQL | Yes |
Oracle Database Server | Not in version 10.2 |
PostgreSQL | Yes, since version 7.4 |
Sybase ASE | Yes |
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 IBM 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 IBM 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. If you are only using one specific database type, you may check if the server supports a versioning column natively. For example, IBM Informix IDS 11.50.xC1 introduced the ALTER TABLE ... ADD VERCOLS option to get a version + checksum column to a table, you can then query the table with the ifx_insert_checksum and ifx_row_version columns.
INFORMIX provides the SERIAL, BIGSERIAL or SERIAL8 data types which can be emulated with ODI drivers for most non-INFORMIX database engines by using native sequence generators (when "ifxemul.serial" is true). But, this requires additional configuration and maintenance tasks. If you plan to review the programming pattern of sequences, you should use a portable implementation instead of the serial emulation provided by the ODI drivers. This section describes different solutions to implement auto-incremented fields. The preferred implementation is the solution using SEQUENCES.
Principle:
In accordance with the target database, you must use the appropriate native serial generation method. Get the database type with the fgl_db_driver_type() built-in function and use the appropriate SQL statements to insert rows with serial generation.
Note that this solution uses the native auto-increment feature of the target database and is fast at execution, but is not very convenient as it requires to write different code for each database type. However, it is covered here to make you understand that each database vendor has it's own specific solution for auto-incremented columns. It is of course not realistic to use this solution in a large application with hundreds of tables.
Implementation:
BDL example:
01
DEFINE t1rec RECORD02
id INTEGER,03
name CHAR(50),04
cdate DATE05
END RECORD06
07
CASE fgl_db_driver_type()08
WHEN "IFX"09
INSERT INTO t1 ( id, name, cdate )10
VALUES ( 0, t1rec.name, t1rec.cdate )11
LET t1rec.id = SQLCA.SQLERRD[2]12
WHEN "ORA"13
INSERT INTO t1 ( id, name, cdate )14
VALUES ( t1seq.nextval, t1rec.name, t1rec.cdate )15
SELECT t1seq.currval INTO t1rec.id INTO dual16
WHEN "MSV"17
INSERT INTO t1 ( name, cdate )18
VALUES ( t1rec.name, t1rec.cdate )19
PREPARE s FROM "SELECT @@IDENTITY"20
EXECUTE s INTO t1rec.id21
END CASE
As you can see in the above example, this solution requires database engine specific coding. Querying the last generated serial can be centralized in a function, but the insert statements would still need to be specific to the type of database.
Purpose:
The goal is to generate unique INTEGER or BIGINT 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.
In order to guarantee the uniqueness of the generated number, the call to db_get_sequence() must be done inside a transaction block that includes the INSERT statement. Concurrent db sessions must wait for each other in case of conflict and the transaction isolation level must be high enough to make sure that the row of the sequence table will not be read or written by other db sessions until the transaction end. Note for example that with Genero db, you must be in the native SERIALIZABLE isolation level, which can be set from the BDL program with SET ISOLATION TO REPEATABLE READ.
BDL example:
01
IMPORT FGL fgldbutl02
DEFINE rec RECORD03
id INTEGER,04
name CHAR(100)05
END RECORD06
...07
BEGIN WORK08
LET rec.id = db_get_sequence( "CUSTID" )09
INSERT INTO CUSTOMER ( CUSTID, CUSTNAME ) VALUES ( rec.* )10
COMMIT WORK
Principle:
More an more database engines support SEQUENCE database objects; If all database server types you want to use do support sequences, you should use this solution.
Note that while writing these lines, Microsoft SQL Server (2008), MySQL (6.0) and SQLite (3) do not support SEQUENCE objects. You can however write stored procedures that emulate sequence generators (For SQL Server, search the MSDN for Oracle SEQUENCE migration tips).
Implementation:
BDL example:
01
IMPORT FGL fgldbutl02
DEFINE dbtype CHAR(3)03
MAIN04
DEFINE item_rec RECORD05
item_num BIGINT,06
item_name VARCHAR(40)07
END RECORD08
DEFINE i INT09
DATABASE test110
LET dbtype = db_get_database_type()11
CREATE TABLE item ( item_num BIGINT NOT NULL PRIMARY KEY, item_name VARCHAR(50) )12
CREATE SEQUENCE item_seq13
LET item_rec.item_num = new_seq("item")14
DISPLAY "New sequence: ", item_rec.item_num15
LET item_rec.item_name = "Item#"|| item_rec.item_num16
INSERT INTO item VALUES ( item_rec.* )17
DROP TABLE item18
DROP SEQUENCE item_seq19
END MAIN20
21
FUNCTION new_seq(tabname)22
DEFINE tabname STRING23
DEFINE sql STRING24
DEFINE newseq BIGINT25
IF dbtype=="PGS" THEN26
LET sql = "SELECT nextval('"||tabname||"_seq')"||unique_row_condition()27
ELSE28
LET sql = "SELECT "||tabname||"_seq.nextval "||unique_row_condition()29
END IF30
PREPARE seq FROM sql31
IF SQLCA.SQLCODE!=0 THEN RETURN -1 END IF32
EXECUTE seq INTO newseq33
IF SQLCA.SQLCODE!=0 THEN RETURN -1 END IF34
RETURN newseq35
END FUNCTION36
37
FUNCTION unique_row_condition()38
CASE dbtype39
WHEN "IFX" RETURN " FROM systables WHERE tabid=1"40
WHEN "DB2" RETURN " FROM sysibm.systables WHERE name='SYSTABLES'"41
WHEN "PGS" RETURN " FROM pg_class WHERE relname='pg_class'"42
WHEN "ORA" RETURN " FROM dual"43
WHEN "ADS" RETURN " FROM dual"44
OTHERWISE RETURN " "45
END CASE46
END FUNCTION
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 IBM Informix, we suggest you keep the database as is, because turning an IBM Informix database into ANSI mode can result in unexpected behavior of the programs.
Here are some ANSI mode issues extracted from the IBM 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.
IBM 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 IBM 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? |
Genero db | Yes (if primary key or unique index) |
IBM DB2 UDB (Unix) | No |
IBM Informix | Yes |
Microsoft SQL Server | No |
MySQL | No |
Oracle Database Server | No |
PostgreSQL | No |
Sybase ASE | 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? |
Genero db | Yes |
IBM DB2 UDB (Unix) | Yes |
IBM 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 ASE | 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 |
Genero db | No |
IBM DB2 UDB (Unix) | No |
IBM Informix | Yes |
Microsoft SQL Server | Yes |
MySQL | No |
Oracle Database Server | No |
PostgreSQL | No |
Sybase ASE | 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 = MDY(12,24,2005)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 = MDY(12,24,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 |
Genero db | [schema.]identifier |
IBM DB2 UDB (Unix) | [[database.]owner.]identifier |
IBM 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 ASE | [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 behavior of each database engine regarding case sensitivity and double quoted identifiers:
Database Server Type | Un-quoted names | Double-quoted names |
Genero db | Converts to uppercase | Case sensitive |
IBM DB2 UDB (Unix) | Converts to uppercase | Case sensitive |
IBM Informix (1) | Converts to lowercase | Syntax disallowed (non-ANSI mode) |
Microsoft SQL Server (2) | Not converted, kept as is | Case sensitive |
MySQL | Not converted, kept as is | Syntax disallowed |
Oracle Database Server | Converts to uppercase | Uppercase |
PostgreSQL | Converts to lowercase | Lowercase |
Sybase ASE | Converts to lowercase | Lowercase |
(1) If not ANSI database mode.
(2) When case-sensitive charset/collation used.
Take care with database servers marked in red, because object identifiers are case sensitive and are not converted to uppercase or lowercase if not delimited by double-quotes. This means that, by error, you can create two tables with a similar name:
CREATE TABLE customer ( cust_id INTEGER ) -- first table CREATE TABLE Customer ( cust_id INTEGER ) -- second table
According to the above facts, it is recommended to design databases with lowercase table and column names.
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? |
Genero db | CREATE TEMP TABLE tablename
( column-defs ) |
Yes |
IBM DB2 UDB (Unix) | DECLARE GLOBAL TEMPORARY
TABLE tablename ( column-defs ) |
Yes |
IBM 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 ASE | CREATE 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 IBM Informix OUTER join syntax |
Genero db | Yes |
IBM DB2 UDB (Unix) | No (but translated by driver) |
IBM 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 ASE | 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? |
Genero db | Yes | SUBSTR(expr,start,length) |
IBM DB2 UDB (Unix) | No | SUBSTR(expr,start,length) |
IBM 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 ASE | No | SUBSTRING(expr,start,length) |
Note that 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? |
Genero db | ROWID |
INTEGER |
IBM DB2 UDB (Unix) | none |
none |
IBM Informix (1) | ROWID |
INTEGER |
Microsoft SQL Server (2) | none |
none |
MySQL | none |
none |
Oracle Database Server | ROWID |
CHAR(18) |
PostgreSQL | OID |
internal type |
Sybase ASE | none |
none |
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? |
Genero db | Yes |
IBM DB2 UDB (Unix) | No |
IBM Informix (1) | Yes |
Microsoft SQL Server (2) | No |
MySQL | No |
Oracle Database Server | No |
PostgreSQL | No |
Sybase ASE | 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.
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, ... ? |
Genero db | No |
IBM DB2 UDB (Unix) | No |
IBM Informix (1) | Yes |
Microsoft SQL Server (2) | No |
MySQL | Yes |
Oracle Database Server | No |
PostgreSQL | Yes |
Sybase ASE | 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 |
Genero db | LENGTH(expr) | No | NULL |
IBM DB2 UDB (Unix) | LENGTH(expr) | Yes | NULL |
IBM 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 ASE (2) | LEN(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 IBM 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 IBM 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 |
Genero db (Since version 3.80) | SQLCancel() | Native error -30005 |
IBM DB2 UDB (Since version 9.x) | SQLCancel() | Native error -952 |
IBM Informix | sqlbreak() | Native error -213 |
Microsoft SQL Server (Only 2005+ with SNC driver) | SQLCancel() | SQLSTATE HY008 |
MySQL | N/A | ? |
Oracle Database Server | OCIBreak() | Native error -1013 |
PostgreSQL | PQCancel() | SQLSTATE 57014 |
Sybase ASE | ct_cancel() | Native error -213 |
IBM Informix IDS 11.50 introduced transaction savepoints, following the ANSI SQL standards. While most recent database severs support savepoints, you must pay attention and avoid Informix specific features. For example, Genero db (3.81), Oracle (11), SQL Server (2008 R2), Sybase ASE (15.5) do not support the RELEASE SAVEPOINT instruction. The UNIQUE clause of SAVEPOINT is only supported by IBM Informix and IBM DB2 UDB.
Database Server Type | SAVEPOINT & ROLLBACK WORK TO SAVEPOINT |
RELEASE SAVEPOINT | SAVEPOINT UNIQUE |
Genero db | Yes | No | No |
IBM DB2 UDB | Yes | Yes | Yes |
IBM Informix | Yes | Yes | Yes |
Microsoft SQL Server (Only 2005+ with SNC driver) | Yes | No | No |
MySQL | Yes | Yes | No |
Oracle Database Server | Yes | No | No |
PostgreSQL | Yes | Yes | No |
Sybase ASE | Yes | No | No |