How to debug a dataset response - soap

Hello Progress4GL Developers,
I am trying to consume a Magento SOAP API and am dealing with a dataset as my output.
Here is the example procedure from the WSDL Analayser:
I am expecting my XML response to look something like this:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:Magento">
<SOAP-ENV:Body>
<ns1:salesOrderListResponseParam>
<result>
<complexObjectArray>
<increment_id>600000018</increment_id>
<store_id>6</store_id>
</complexObjectArray>
<complextObjectArray>
<increment_id>600000019</increment_id>
<store_id>7</store_id>
</complexObjectArray>
</result>
</ns1:salesOrderListResponseParam>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
The current code I have is the following where I am expecting an output in the resultTable temp-table (but nothing is currently being returned).
define variable hs as handle no-undo.
define variable hp as handle no-undo.
DEFINE VARIABLE cUsername AS CHARACTER INIT "USERNAME".
DEFINE VARIABLE cPassword AS CHARACTER INIT "PASSWORDAPIKEY".
DEFINE VARIABLE oSession AS CHARACTER NO-UNDO.
DEFINE VARIABLE iRequest AS LONGCHAR NO-UNDO.
DEFINE TEMP-TABLE complexObjectArray NO-UNDO
NAMESPACE-URI ""
FIELD key AS CHARACTER
FIELD value1 AS CHARACTER
XML-NODE-NAME "value"
FIELD filter_id AS RECID
XML-NODE-TYPE "HIDDEN".
CREATE complexObjectArray.
assign
complexObjectArray.key = "status"
complexObjectArray.value1 = "pending".
DEFINE DATASET filter NAMESPACE-URI ""
FOR complexObjectArray.
DEFINE TEMP-TABLE resultTable NO-UNDO
NAMESPACE-URI "complexObjectArray"
FIELD increment_id AS CHARACTER.
DEFINE DATASET resultData NAMESPACE-URI "salesOrderListResponseParam"
FOR resultTable.
create server hs.
hs:connect( "-WSDL WSDLADDRESSHERE" ).
run PortType set hp on server hs.
run login in hp ( input cUsername, input cPassword, output oSession ).
message oSession . PAUSE 100.
run salesOrderList in hp ( input oSession, input dataset filter, output dataset resultData ).
for each resultTable:
disp resultTable with 2 col.
end.
delete procedure hp.
hs:disconnect().
delete object hs.
There are no errors, but it fails to show a response in the temp-table.
Many Thanks in Advance!

First of all you should try to create a dataset that exactly matches your expected output. I would do it something like this.
I'm unsure about the namespace, ns1. You could try to include it in the XML-NODE-NAME or via the NAMESPACE-PREFIX attribute. But it might work even if you just leave it out.
DEFINE TEMP-TABLE ttResult NO-UNDO
XML-NODE-NAME "result"
FIELD id AS INTEGER SERIALIZE-HIDDEN.
DEFINE TEMP-TABLE ttcomplexObjectArray NO-UNDO
XML-NODE-NAME "complexObjectArray"
FIELD id as RECID SERIALIZE-HIDDEN
FIELD increment_id AS CHARACTER
FIELD store_id AS CHARACTER.
DEFINE DATASET dsSalesOrderListResponseParam XML-NODE-NAME "salesOrderListResponseParam"
FOR ttResult, ttcomplexObjectArray
PARENT-ID-RELATION pr1 FOR ttResult, ttcomplexObjectArray PARENT-ID-FIELD id.
DATASET dsSalesOrderListResponseParam:READ-XML("file", "c:\temp\xml.xml", ? , ? , ?).
This works with the "body" part of your expected response (XML-Envelope etc excluded) as below.
<?xml version="1.0" ?>
<salesOrderListResponseParam >
<result>
<complexObjectArray>
<increment_id>600000018</increment_id>
<store_id>6</store_id>
</complexObjectArray>
<complexObjectArray>
<increment_id>600000019</increment_id>
<store_id>7</store_id>
</complexObjectArray>
</result>
</salesOrderListResponseParam>
Start with saving your response as the dataset sees it:
dsSalesOrderListResponseParam:WRITE-XML("file", "response.xml").
Then you can look at the file and see what's there. That's a start at least!

Thanks #Jensd for your answer. Using the write-xml statement really helps to identify how the dataset is structured.
There were three problems with my code above.
It needed the SERIALIZE-NAME statement in the temp-tables.
The filter was not being created correctly, and by using the write-xml statement I was able to identify what was going wrong.
I needed to assign a filter_id to the complexobjectarray which is equal to the recid of the filter temptable (which basically nests the XML as needed).
Please find below the code which resolved this issue (should it be of use to anybody else trying to do this):
define variable hs as handle no-undo.
define variable hp as handle no-undo.
DEFINE VARIABLE cUsername AS CHARACTER INIT "USER".
DEFINE VARIABLE cPassword AS CHARACTER INIT "PASS".
DEFINE VARIABLE oSession AS CHARACTER NO-UNDO.
DEFINE VARIABLE iRequest AS LONGCHAR NO-UNDO.
/* creating the filter */
DEFINE TEMP-TABLE filter NO-UNDO
FIELD filter_field AS INTEGER
XML-NODE-TYPE "HIDDEN" .
DEFINE TEMP-TABLE complexObjectArray NO-UNDO
SERIALIZE-NAME "complexObjectArray"
FIELD key AS CHARACTER
FIELD value1 AS CHARACTER
XML-NODE-NAME "value"
FIELD filter_id AS RECID
XML-NODE-TYPE "HIDDEN" .
CREATE filter.
CREATE complexObjectArray.
assign
complexObjectArray.key = "status"
complexObjectArray.value1 = "pending"
complexObjectArray.filter_id = RECID(filter).
.
DEFINE DATASET filters
FOR filter, complexObjectArray
PARENT-ID-RELATION RELATION1 FOR filter, complexObjectArray
PARENT-ID-FIELD filter_id.
/* debug dataset filters */
DATASET filters:WRITE-XML("file", "/home/jbetts/request.xml", ? , ? , ?).
DEFINE TEMP-TABLE resultTable NO-UNDO
SERIALIZE-NAME "complexObjectArray"
FIELD increment_id AS CHARACTER.
DEFINE DATASET resultData SERIALIZE-NAME "salesOrderListResponseParam"
FOR resultTable.
create server hs.
hs:connect( "-WSDL WSDLHERE" ).
run PortType set hp on server hs.
run login in hp ( input cUsername, input cPassword, output oSession ).
message oSession . PAUSE 100.
run salesOrderList in hp ( input oSession, input dataset filters, output dataset resultData ).
for each resultTable:
disp resultTable.increment_id format "x(30)".
end.
find first resultTable no-lock no-error.
if not available resulttable then message "no results".
delete procedure hp.
hs:disconnect().
delete object hs.

Related

CREATE FUNCTION failed because a column name is not specified for column 1. error for the Multiple parameter of function

Wanted to create the multiple parameter of function but it gives me this error:
CREATE FUNCTION failed because a column name is not specified for
column 1.
Code below:
create function dmt.Impacted(
#nameOfColumn varchar , #nameOfParam varchar)
returns table
as
return
(select
case when '['+#nameOfColumn+']' is null or len(rtrim('['+#nameOfColumn+']')) = 0
then Convert(nvarchar(2),0)
else
#nameOfParam end from employee) ;
As the error message clearly said, the column in the returned result need a name. Either give it an alias in the SELECT like
SELECT CASE
...
END a_column_name
...
or define it in the declaration of the return type as in
...
RETURNS TABLE
(a_column_name nvarchar(max)
...
As you can see in the second form you have to specify a data type. As your current code doesn't make much sense now I cannot figure out what is the right one there. You'd need to amend it.
Note, that len(rtrim('['+#nameOfColumn+']')) = 0 is never true as len(rtrim('['+#nameOfColumn+']')) is either NULL, when #nameOfColumn is NULL or at least 2 because of the added brackets.
If #nameOfColumn is supposed to be a column name you shouldn't use varchar (especially without a length specified for it) but sysname which is a special type for object names.
Either way you should define a length for #nameOfColumn and #nameOfParam as just varchar without any length means varchar(1), which is probably not what you want. And maybe instead of varchar you want nvarchar.
You may also want to look into quotename().
Define name of column in SELECT statement :
(select case when '['+#nameOfColumn+']' is null or
len(rtrim('['+#nameOfColumn+']')) = 0
then Convert(nvarchar(2),0)
else #nameOfParam
end as name_column -- define column name
from employee)
Also, your function parameter has no data length, by default it will accept only 1 character #nameOfColumn varchar , #nameOfParam varchar & rest will trim.

Get temp-table's name in code

I'm trying to get this temp-table's name/variablename ('TT_Test') in code so I could compare it:
DEFINE TEMP-TABLE TT_Test NO-UNDO
FIELD Test AS CHAR
.
I've tried to use this without success:
PROCEDURE testProc:
DEF VAR name AS CHAR NO-UNDO.
name = TT_Test:NAME.
END PROCEDURE.
Is there any way to get it?
cName = TEMP-TABLE tt_test:NAME.

Creating Database Table From Temp-Table (by Code)

I have a temp-table called tt. I want to create a database table with the same field names and types using my temp-table.
I can't figure out how to do in Progress-4gl. Is this possible ?
Thanks.
Short answer: yes
The safest way is to do this by code is to create an incremental df and then load this.
Here is a very partial start that should get you going:
DEFINE TEMP-TABLE tt NO-UNDO
FIELD ii AS INT
FIELD cc AS CHAR
INDEX ttix IS UNIQUE PRIMARY ii.
DEF VAR hb AS HANDLE NO-UNDO.
DEF VAR hf AS HANDLE NO-UNDO.
DEF VAR ifield AS INT NO-UNDO.
hb = TEMP-TABLE tt:DEFAULT-BUFFER-HANDLE.
OUTPUT TO "tt.df".
PUT UNFORMATTED SUBSTITUTE( "ADD TABLE &1", QUOTER( hb:NAME ) ) SKIP.
DO ifield = 1 TO hb:NUM-FIELDS:
hf = hb:BUFFER-FIELD( ifield ).
PUT UNFORMATTED
SUBSTITUTE(
"ADD FIELD &1 OF &2 AS &3",
QUOTER( hf:NAME ),
QUOTER( hb:NAME ),
hf:DATA-TYPE
) SKIP.
/* to do: add other field attributes like label, initial value, decimals, format */
END.
/* to do: add indices */
OUTPUT CLOSE.
The resulting df can be loaded with:
RUN prodict/load_df.p ( "tt.df" ).
While this might be possible it's not a very good idea. Chances are you will corrupt your database. This might lead to data loss. You are much better off sticking to the provided tools even if they might have some problems.
Backup first
Always back up before applying any changes!
Single Use Database
You can modify a single usage database (if you're locally connected) online directly in the Data Dictionary. This is quite straight forward. Just use the GUI to add the table, it's fields and indices. Make sure you commit your changes.
Multi User Database
A multi user database can be changed offline or (a little bit more limited) online. Adding a new table with corresponding fields and indices is a change that can be made online. Adding the changes offline is done exactly in the same way as the "Single Use Database".
Online changes requires more work:
First of all you need two connected databases. I'll call them "live" and "development" in this case. "Development" will consist of a perfect copy of "live" when it comes to the schema. It doesn't have to have any data in it. The databases can have the same name - then you'll use a "logical name" alias when connecting to one of them.
Make the changes in "development" just like you would in an offline database or single usage database (use Data Dictionary). Make sure you commit your changes.
Enter the Data Administration Tool. Make sure you have "development" selected as default database. Go to the Admin -> Dump Data and Definitions -> Create Incremental .df file. Select your "live" database as comparative database and select a good file name (delta.df is default).
The file created is a diff-file between your "live" and "development" databases. It's wise to check it to make sure you're not dropping any tables or anything like that by mistake.
Now select the "live" database as default and go to Admin -> Load Data and Definitions. Here you can choose to add the changes offline (default) or online (check 'Add new objects on-line').
Some schema changes will require you to compile your source code again!
Using the example of #Stefan Drissen, and done some modification.
{temp-tables.i}
def input param table-handle tt.
def var hb as handle no-undo.
def var hf as handle no-undo.
def var ifield as int no-undo.
def var vindex as char no-undo.
def var vi as int.
hb = tt:default-buffer-handle.
output to value("/home/user/teste.df") append.
put unformatted substitute( "ADD TABLE &1", quoter(hb:name)) skip.
DO ifield = 1 to hb:num-fields:
hf = hb:buffer-field( ifield ).
put unformatted
substitute(
"ADD FIELD &1 OF &2 AS &3",
quoter( hf:name ),
quoter( hb:name ),
hf:data-type
) skip.
put unformatted
substitute(
"FORMAT &1",
quoter(hf:format)
) skip.
put unformatted
substitute (
"LABEL &1",
quoter( hf:name )
) skip.
put unformatted "ORDER " + string(ifield * 10) skip.
if ifield < hb:num-fields then put unformatted skip (1).
end.
put unformatted skip(1).
/*=== INDEX CREATION ===*/
assign vindex = hb:index-information(1).
if lookup("default",vindex) <= 0
then do:
put unformatted(
substitute(
"ADD INDEX &1 ON &2",
quoter(entry(01,vindex,",")),
quoter( hb:name ))) skip.
if entry(02,vindex,",") = "1"
then put unformatted "UNIQUE" skip.
if entry(03,vindex,",") = "1"
then put unformatted "PRIMARY" skip.
do vi = 5 to 20 by 2:
if num-entries(vindex,",") > vi
then put unformatted "INDEX-FIELD " entry(vi,vindex,",") " " (if entry(vi + 1,vindex,",") = "0" then "ASCENDING" else "DESCENDING") skip.
end.
end.
/*=== INDEX CREATION ===*/
put unformatted skip(1).
output close.
To make it work, just call this code on a program.p.
run createDF.p(input table nameOfYourTempTable).
CREATE TABLE new_table
AS (SELECT * FROM tt);
Just replace the new_table with the table name you want to give

Query a database table whose name is known only at runtime

I get a database table name at run time(let us suppose from user). I need to query the table and return few fields(which I know). How to do this?
"FOR EACH" wont accept a variable name in it. So, I cant use it.
I have gone through dynamic queries, especially SET-BUFFERS() method. Even with this, I need to know the table name before.
I need something like:
DEF VAR tablename AS CHAR.
tablename = somename.
FOR EACH tablename WHERE ....:
...
...
END.
Can someone please point me to right direction?
You can do a dynamic query with a dynamic buffer. Simply replace the value of cTableName variable in this example:
/* Replace _file with whatever field you're after */
DEFINE VARIABLE cTableName AS CHARACTER NO-UNDO INIT "_file".
DEFINE VARIABLE hQuery AS HANDLE NO-UNDO.
DEFINE VARIABLE hBuffer AS HANDLE NO-UNDO.
CREATE BUFFER hBuffer FOR TABLE cTableName.
CREATE QUERY hQuery.
hQuery:SET-BUFFERS(hBuffer).
hQuery:QUERY-PREPARE("FOR EACH " + cTableName).
hQuery:QUERY-OPEN().
REPEAT:
hQuery:GET-NEXT().
IF hQuery:QUERY-OFF-END THEN LEAVE.
DISPLAY hBuffer:BUFFER-FIELD(1):BUFFER-VALUE.
/* If you know the name of the field you can do: */
/* DISPLAY hBuffer:BUFFER-FIELD("nameoffield"):BUFFER-VALUE. */
END.
/* Clean up */
hQuery:QUERY-CLOSE().
hBuffer:BUFFER-RELEASE().
DELETE OBJECT hBuffer.
DELETE OBJECT hQuery.

Returning to a text field block again from the query

I have a form that prompt for customer name and pass that value to a query,
FORM compname
customer.cusname
WITH FRAME f1.
UPDATE compname WITH FRAME f1.
This form wil pass the compname value to the following query,
FOR EACH customer WHERE customer.name = compname NO-LOCK :
if available(company) then
do:
CREATE temptt.
assign temptt.num = customer.kco
temptt.no = string(customer.kco)
temptt.name = customer.name
temptt.status = false.
END.
else
message "not matched " view-as alert-box.
end.
What i want to do is, if search does not receive any rows, it should again prompt for customer name. what should i do for this ??
how do i call that form again in the "else block" and also, currently I am giving the complete name in the field, but i want to give part of the name, for eg., customer name is "John Smith Doe" and if i input "Smith" it should retrieve the related rows. How should i alter the "Where" clause for this ?? Please help me.
Repeating the search
This can be done in several ways. Here's one example:
DEFINE TEMP-TABLE customer NO-UNDO
FIELD cusname AS CHARACTER
FIELD num AS INTEGER.
DEFINE VARIABLE compnum AS INTEGER NO-UNDO.
DEFINE VARIABLE compname AS CHARACTER NO-UNDO.
DEFINE QUERY qSearch FOR customer.
FORM compname compnum WITH FRAME f1.
/* Create some bogus data */
CREATE customer.
ASSIGN customer.cusname = "john doe"
customer.num = 1.
CREATE customer.
ASSIGN customer.cusname = "jane doe"
customer.num = 2.
CREATE customer.
ASSIGN customer.cusname = "name name"
customer.num = 3.
loop:
REPEAT:
CLEAR FRAME f2 ALL.
UPDATE compname compnum WITH FRAME f1.
/* Quit if neither name or number is entered */
IF compname = "" AND compnum = 0 THEN
LEAVE loop.
/* If num is entered - search by it, otherwise by name */
IF compnum <> 0 THEN DO:
OPEN QUERY qSearch FOR EACH customer NO-LOCK WHERE customer.num = compnum.
END.
ELSE DO:
OPEN QUERY qSearch FOR EACH customer NO-LOCK WHERE customer.cusname MATCHES "*" + compname + "*".
END.
GET NEXT qSearch.
DO WHILE AVAILABLE customer:
IF AVAILABLE customer THEN DO:
DISPLAY customer WITH FRAME f2 10 DOWN.
DOWN WITH FRAME f2.
END.
GET NEXT qSearch.
END.
/* If we have results - leave the loop otherwise try again */
IF QUERY qSearch:NUM-RESULTS = 0 THEN
LEAVE loop.
END.
MESSAGE "Quitting" VIEW-AS ALERT-BOX.
Searching for part of the name
There are a couple of operators for matching strings:
BEGINS
Tests a character expression to see if that expression begins with a second character expression.
Syntax:
expression1 BEGINS expression2
Example:
FOR EACH customer WHERE NO-LOCK customer.cusname BEGINS "john":
MATCHES
Compares a character expression to a pattern and evaluates to a TRUE value if the expression satisfies the pattern criteria.
The pattern can contain wildcard characters: a period (.) in a particular position indicates that any single character is acceptable in that position; an asterisk (*) indicates that any group of characters is acceptable, including a null group of characters.
Syntax:
expression1 MATCHES expression2
Example:
FOR EACH customer NO-LOCK WHERE customer.cusname MATCHES "*doe*":
MATCHES sounds like what you're after but be adviced: MATCHES will not utilize indices in the database so whole tables will be scanned. This can/will effect performance and possibly make your queries take long time.
The WHERE clause above replaced with MATCHES would look something like this:
FOR EACH customer NO-LOCK WHERE customer.cusname MATCHES "*" + compname + "*":
CONTAINS
There's also a third operator called CONTAINS that uses something called a WORD index. That will require you or your DBA to create these kind of indices in the database first. Read more about word indices and CONTAINS in the online help or in the PDF found here: Progress ABL Reference (page 1004).
CONTAINS is probably a better idea than MATCHES but will require you to make changes to your database as well.