Unexpected behaviour in nested COND statement - abap

I'm using the conditional operator COND in a string template to create a string to concatenate a variable Alpha and Beta. If Alpha is initial the string is initial but if it isn't the result should be 'Alpha' if Beta is initial but 'Alpha-Beta' if Beta is not initial.
This is the code:
DATA:
lv_alpha TYPE string VALUE 'Alpha',
lv_beta TYPE string VALUE 'Beta',
lv_result TYPE string.
lv_result = COND #( WHEN lv_alpha IS INITIAL THEN '' ELSE
|{ lv_alpha }{ COND #( WHEN lv_beta IS INITIAL THEN '' ELSE |-{ lv_beta }| ) }|
).
The problem now is that when I run this, lv_result actually returns: 'Alpha-', so the value of lv_beta is not included, even though I know that the code entered the second part of the inner COND statement. What is causing this?

You see this behaviour when you use # as the operand type and the result of the type inference doesn't match what you want it to.
If you look at the documentation on Type Inference for Actual Parameters it explains:
If a constructor expression COND|SWITCH #( ... THEN ... ) is passed to generically typed formal parameters as an actual parameter using the character # as a symbol for the operand type, the following type inference is performed for the character #:
If the data type of the operand after the first THEN is identifiable statically and matches the generic type of the formal parameter, this data type is used.
If the data type of the operand after the first THEN is identifiable statically and does not match the generic type of the formal parameter or of it is not identifiable statically, the type is derived from the generic type as follows:
The fact that you're seeing this on the inner COND and not the outer one is because the outer type inference looks to the type of lv_result, a string. The inner COND has no direct type to look at so instead uses the statically identifiable type '', defaulting to a CHAR(1).
You can easily observe this behaviour by running the following commands. The output of each is behind the ".
DATA(lv_test) = 'ABCDEFG'.
WRITE :/ COND #( WHEN lv_alpha IS INITIAL THEN '' ELSE lv_test ). " A
WRITE :/ COND #( WHEN lv_alpha IS INITIAL THEN '123' ELSE lv_test ). " ABC
WRITE :/ COND string( WHEN lv_alpha IS INITIAL THEN '' ELSE lv_test ). " ABCDEFG
In both of the first commands, the type is derived from the first operand after the THEN and as a result the length of the output is limited based on the length of that first operand. The last line correctly specifies that we want to see a string as the result and ignores whatever type the first operand has. You're more likely to see this in nested statements because of how the type is inferred. Most programmers always use fully typed variables which means that errors like in the first two statements here are rare.
The # operand type is very convenient and certainly speeds up programming but you need to keep in mind what it's actually doing or risk running into strange bugs like these. These last examples show what happens when you rely on an unspecified type and you're passing in a value of a different type than what you're expecting, but the result won't always be this obviously incorrect, especially since this can happen with all data types, not just CLIKEs. Especially in nested statements where you can't view the type of the intermediate objects, this can be very difficult to trace.
DATA: lv_date TYPE dats.
WRITE :/ COND #( WHEN lv_alpha IS INITIAL THEN lv_date ELSE lv_test ). " G .EF.ABCD
WRITE :/ COND dats( WHEN lv_alpha IS INITIAL THEN '' ELSE lv_test ). " G .EF.ABCD
WRITE :/ COND string( WHEN lv_alpha IS INITIAL THEN lv_date ELSE lv_test ). " ABCDEFG
WRITE :/ COND string( WHEN lv_alpha IS INITIAL THEN '' ELSE lv_test ). " ABCDEFG

Related

Difference between ORA-12899 and ORA-01480

I would like to understand the difference between ORA-12899 and ORA-01480
ORA-12899: value too large for column
ORA-01480: trailing null missing from STR bind value
Based on my understanding, I know about ORA-12899 and how it can come. Lets say if datatype for some column is VARCHAR2(100 BYTE) and I am trying to insert more than 100 BYTE in the column then I am getting ORA-12899.
What about ORA-01480 ? I searchthe ed on internet and and the similar explanation like ORA-12899
From google :
ORA-01480: trailing null missing from STR bind value
Cause: A bind variable of type 5 (null-terminated string) does not contain the terminating null in its buffer.
Maybe you're trying to insert a string in a column that is bigger than the column length. So, the terminating character is not being
inserted at the end of the string.
Both ORA-12899 and ORA-01480 look similar. Can someone please explain the exact difference with an example?
The error:
ORA-01480: trailing null missing from STR bind value
Is for languages such as C or C++ (among many others) which allow low level manipulation of string-like objects.
If you have a VARCHAR2(100) column and you try to pass, to a bind variable, the byte array:
['h','e','l','l','o']
Then you may get the ORA-01480 error because a NUL terminated string is expected and it should have been:
['h','e','l','l','o','\0']
And the NUL character is expected as it allows the SQL engine to determine the length of the VARCHAR2 variable length string.
An example is using Pro*C:
char s[2] = "ab";
EXEC SQL SELECT :s into :s2 FROM dual;
Outputs: ORA-01480: trailing null missing from STR bind value
char s[3] = "ab\0";
EXEC SQL SELECT :s into :s2 FROM dual;
Works.

Casting of structure to string fails when structure contains a String field

I have a dynamic internal table <ft_dyn_tab>. I want to cast each row of the internal table to the type string via the field symbol <lf_string>:
LOOP AT <ft_dyn_tab> ASSIGNING <fs_dyn_wa>.
ASSIGN <fs_dyn_wa> to <lf_string> CASTING.
...
"other logic
...
ENDLOOP.
Normally, CASTING works fine when all fields of the structure are of type character. But when one field is of type string, it gives a runtime error. Can anyone explain why? And how to resolve this issue?
Why a structure with only character-like and String components can't be "casted" as a text variable
The reason is given by the ABAP documentation of Strings:
"A structure that contains a string is a deep structure and cannot be used as a character-like field in the same way as a flat structure.".
and of Deep:
"Deep: [...] the content [...] is addressed internally using references ([...], strings..."
and of Memory Requirement for Deep Data Objects:
"The memory requirement for the reference is 8 byte. [...] In strings, [...] an implicit reference is created internally."
and of ASSIGN - casting_spec:
"If the data type determined by CASTING is deep or if deep data objects are stored in the assigned memory area, the deep components must appear with exactly the same type and position in the assigned memory area. In particular, this means that individual reference variables can be assigned to only one field symbol that is typed as a reference variable by the same static type."
Now, the reason why the compiler and the run time don't let you do that, is that if you cast a whole deep structure, you could change the 8-bytes reference to access any place in the memory, that could be dangerous (How dangerous is it to access an array out of bounds?) and very difficult to analyze the subsequent bugs. In all programming languages, as far as possible, the compiler prevents out-of-bounds accesses or the checks are done at run time (Bounds checking).
Workaround
Your issue happens at run time because you use dynamically-created data objects, but you'd have exactly the same issue at compile time with statically-defined data objects. Below is a simple solution with a statically-defined structure.
You can access each field of the structure and concatenate it to a string:
DATA: BEGIN OF dyn_wa,
country TYPE c LENGTH 3,
city TYPE string,
END OF dyn_wa,
lf_string TYPE string.
FIELD-SYMBOLS: <lf_field> TYPE clike.
dyn_wa = VALUE #( country = 'FR' city = 'Paris' ).
DO.
ASSIGN COMPONENT sy-index OF STRUCTURE dyn_wa TO <lf_field>.
IF sy-subrc <> 0.
EXIT.
ENDIF.
CONCATENATE lf_string <lf_field> INTO lf_string RESPECTING BLANKS.
ENDDO.
ASSERT lf_string = 'FR Paris'. " one space because country is 3 characters
RESPECTING BLANKS keeps trailing spaces, to mimic ASSIGN ... CASTING.
Sounds like you want to assign the complete structured row to a plain string field symbol. This doesn't work. You can only assign the individual type-compatible components of the structured row to the string field symbol.
Otherwise, this kind of assignment works fine. For a table with a single column with type string:
TYPES table_type TYPE STANDARD TABLE OF string WITH EMPTY KEY.
DATA(filled_table) = VALUE table_type( ( `Test` ) ).
ASSIGN filled_table TO FIELD-SYMBOL(<dynamic_table>).
FIELD-SYMBOLS <string> TYPE string.
LOOP AT <dynamic_table> ASSIGNING FIELD-SYMBOL(<row>).
ASSIGN <row> TO FIELD-SYMBOL(<string>).
ENDLOOP.
For a table with a structured row type:
TYPES:
BEGIN OF row_type,
some_character_field TYPE char80,
the_string_field TYPE string,
END OF row_type.
TYPES table_type TYPE STANDARD TABLE OF row_type WITH EMPTY KEY.
DATA(filled_table) = VALUE table_type( ( some_character_field = 'ABC'
the_string_field = `Test` ) ).
ASSIGN filled_table TO FIELD-SYMBOL(<dynamic_table>).
FIELD-SYMBOLS <string> TYPE string.
LOOP AT <dynamic_table> ASSIGNING FIELD-SYMBOL(<row>).
ASSIGN <row>-the_string_field TO <string>.
ENDLOOP.
I have just tested this and it gives runtime error also when the structure does not have any string typed field.
I change the ASSIGN to a simple MOVE to a string variable g_string and it fails with runtime. If this fail it means that such an assignment is not possible, so the casting will not be either.
REPORT ZZZ.
TYPES BEGIN OF t_test.
TYPES: f1 TYPE c LENGTH 2,
f2 TYPE n LENGTH 4,
f3 TYPE string.
TYPEs END OF t_test.
TYPES BEGIN OF t_test2.
TYPES: f1 TYPE c LENGTH 2,
f2 TYPE n LENGTH 4,
f3 TYPE c LENGTH 80.
TYPES END OF t_test2.
TYPES: tt_test TYPE STANDARD TABLE OF t_test WITH EMPTY KEY,
tt_test2 TYPE STANDARD TABLE OF t_test2 WITH EMPTY KEY.
DATA(gt_test) = VALUE tt_test( ( f1 = '01' f2 = '1234' f3 = `Test`) ).
DATA(gt_test2) = VALUE tt_test2( ( f1 = '01' f2 = '1234' f3 = 'Test') ).
DATA: g_string TYPE string.
FIELD-SYMBOLS: <g_any_table> TYPE ANY TABLE,
<g_string> TYPE string.
ASSIGN gt_test2 TO <g_any_table>.
ASSERT <g_any_table> IS ASSIGNED.
LOOP AT <g_any_table> ASSIGNING FIELD-SYMBOL(<g_any_wa2>).
* ASSIGN <g_any_wa2> TO <g_string> CASTING.
g_string = <g_any_wa2>.
ENDLOOP.
UNASSIGN <g_any_table>.
ASSIGN gt_test TO <g_any_table>.
ASSERT <g_any_table> IS ASSIGNED.
LOOP AT <g_any_table> ASSIGNING FIELD-SYMBOL(<g_any_wa>).
* ASSIGN <g_any_wa> TO <g_string> CASTING.
g_string = <g_any_wa>.
ENDLOOP.

RTTI: Get length of a character column

My function module receives a table name and a column name at runtime.
I would like to get the length of the column: How many characters are allowed in the transparent table?
I used my favorite search engine and found RTTS.
But the examples in the documentation pass a variable to the RTTS method DESCRIBE_BY_DATA; in my case, I don't have a variable, I just have the type names in table_name and column_name.
How to get the length?
For retrieving the type of a given DDIC type only known at runtime, use the method DESCRIBE_BY_NAME. The RTTI length is always returned as a number of bytes.
Example to get the type of the column CARRID of table SFLIGHT (I know it's a column of 3 characters) :
cl_abap_typedescr=>describe_by_name(
EXPORTING
p_name = 'SFLIGHT-CARRID'
RECEIVING
p_descr_ref = DATA(lo_typedescr)
EXCEPTIONS
type_not_found = 1 ).
" you should handle the error if SY-SUBRC <> 0
" Because it's SFLIGHT-CARRID, I expect 6 BYTES
ASSERT lo_typedescr->length = 6. " 3 characters * 2 bytes (Unicode)
" Length in CHARACTERS
CASE lo_typedescr->type_kind.
WHEN lo_typedescr->typekind_char
OR lo_typedescr->typekind_num
OR lo_typedescr->typekind_date
OR lo_typedescr->typekind_time
OR lo_typedescr->typekind_string.
DATA(no_of_characters) = lo_typedescr->length / cl_abap_char_utilities=>charsize.
ASSERT no_of_characters = 3.
ENDCASE.
You don't need RTTS for this. You can Do one of this
Select table DD03L with TABNAME and FIELDNAME
Use Function DDIF_FIELDINFO_GET

Quoting arguments in dynamic WHERE in OpenSQL

I found this example how to create a dynamic WHERE:
REPORT ZII_RKP_TEST1.
DATA: cond(72) TYPE c,
itab LIKE TABLE OF cond.
PARAMETERS: source(10) TYPE c, dest(10) TYPE c.
DATA wa TYPE spfli-cityfrom.
CONCATENATE 'CITYFROM = ''' source '''' INTO cond.
APPEND cond TO itab.
CONCATENATE 'OR CITYFROM = ''' dest '''' INTO cond.
APPEND cond TO itab.
CONCATENATE 'OR CITYFROM = ''' 'BAYERN' '''' INTO cond.
APPEND cond TO itab.
LOOP AT itab INTO cond.
WRITE cond.
ENDLOOP.
SKIP.
SELECT cityfrom
INTO wa
FROM spfli
WHERE (itab).
WRITE / wa.
ENDSELECT.
Source: https://wiki.scn.sap.com/wiki/display/ABAP/Dynamic%2Bwhere%2Bclause
Above example uses static values like "BAYERN", but if I use arbitrary values, then I guess things could break for some special values like '''.
Is it necessary to do some quoting to make the dynamic WHERE unbreakable? And if yes, how to do it?
You can escape apostrophe in perform before adding them like below
PERFORM escape CHANGING source.
PERFORM escape CHANGING dest.
CONCATENATE 'CITYFROM = ''' source '''' INTO cond.
APPEND cond TO itab.
...
FORM escape CHANGING value TYPE c.
REPLACE ALL OCCURRENCES OF '''' IN value WITH ''''''.
ENDFORM.
Latest ABAP versions included escape function details are here. But it is not included quote escaping. We can use static escape_quotes method on class cl_abap_dyn_prg like below.
CALL METHOD cl_abap_dyn_prg=>escape_quotes
EXPORTING
val = source
receiving
out = output.
Method making something look like above perform.
I wonder why that example is written that way, maybe it's an old piece of code.
1st: if you have access in your system to the "new" string syntax, you can just use something like
WHERE = |CITYFROM = '| && source && |'|.
2nd: the nice thing about string WHERE clauses is that you can just use variables as part of the string, I mean, if you just write something like
WHERE = 'CITYFROM = source'.
ABAP will transform that into proper SQL, as if you were writing your SQL properly.
(I wish I could explain myself properly, do not hesitate to ask if you have any doubt)
The method cl_abap_dyn_prg=>quote( name ) should get used.
Example:
DATA(cond) = `country = 'DE' AND name = ` &&
cl_abap_dyn_prg=>quote( name ).
Source: SQL Injections Using Dynamic Tokens
Thanks to Sandra Rossi for pointing me in the right direction.

Converting char to integer in INSERT using IIF and SIMILAR TO

I am using in insert statement to convert BDE table (source) to a Firebird table (destination) using IB Datapump. So the INSERT statement is fed by source table values via parameters. One of the source field parameters is alphanum (SOURCECHAR10 char(10), holds mostly integers and needs to be converted to integer in the (integer type) destination column NEWINTFLD. If SOURCECHAR10 is not numeric, I want to assign 0 to NEWINTFLD.
I use IIF and SIMILAR to to test whether the string is numeric, and assign 0 if not numeric as follows:
INSERT INTO "DEST_TABLE" (......, "NEWINTFLD",.....)
VALUES(..., IIF( :"SOURCECHAR10" SIMILAR TO '[[:DIGIT:]]*', :"SOURCECHAR10", 0),..)
For every non numeric string however, I still get conversion errors (DSQL error code = -303).
I tested with only constants in the IIF result fields like SOURCECHAR10" SIMILAR TO '[[:DIGIT:]]*', 1, 0) and that works fine so somehow the :SOURCECHAR10 in the true result field of the IIF generates the error.
Any ideas how to get around this?
When your query is executed, the parser will notice that second use of :"SOURCECHAR10" is used in a place where an integer is expected. Therefor it will always convert the contents of :SOURCECHAR10 into an integer for that position, even though it is not used if the string is non-integer.
In reality Firebird does not use :"SOURCECHAR10" as parameters, but your connection library will convert it to two separate parameter placeholders ? and the type of the second placeholder will be INTEGER. So the conversion happens before the actual query is executed.
The solution is probably (I didn't test it, might contain syntax errors) to use something like (NOTE: see second example for correct solution):
CASE
WHEN :"SOURCECHAR10" SIMILAR TO '[[:DIGIT:]]*'
THEN CAST(:"SOURCECHAR10" AS INTEGER)
ELSE 0
END
This doesn't work as this is interpreted as a cast of the parameter itself, see CAST() item 'Casting input fields'
If this does not work, you could also attempt to add an explicit cast to VARCHAR around :"SOURCECHAR10" to make sure the parameter is correctly identified as being VARCHAR:
CASE
WHEN :"SOURCECHAR10" SIMILAR TO '[[:DIGIT:]]*'
THEN CAST(CAST(:"SOURCECHAR10" AS VARCHAR(10) AS INTEGER)
ELSE 0
END
Here the inner cast is applied to the parameter itself, the outer cast is applied when the CASE expression is evaluated to true