Error: PLS-00103: Encountered the symbol "/" - stored-procedures

I am new to Pl Sql. I am trying to run procedure and getting these errors:
Encountered the symbol "Create" with begin case declare end exception exit for goto if loop mod null pragma raise return select update while with
<< continue close current delete fetch lock
insert open rollback savepoint set sql execute commit forall
merge pipe purge:
I have tried searching for what causes these errors and for examples similar to this, but results were not sufficient. And this is my script:
CREATE OR REPLACE PROCEDURE CommandScript IS
BEGIN
EXECUTE IMMEDIATE 'DROP TABLE sf_tmp';
EXCEPTION
WHEN OTHERS THEN IF SQLCODE != -942
THEN RAISE;
END IF;
create table sf_tmp(smcard varchar2(17),p_line varchar2(1000));
INSERT INTO dec_tmp(DECSCNR,DECSCPSNR)
SELECT TRIM (SUBSTRB (qinputstring, INSTR (qinputstring, '|', 1, 2) + 1,11)) dec_nr,
decoders.decscpsnr FROM bgqueue, decoders WHERE qworktype = 10
AND decoders.decscpsnr > 0 AND decoders.decscnr =
TRIM (SUBSTRB (qinputstring,INSTR (qinputstring, '|', 1, 2) + 1,11))
AND LENGTH (TRIM (SUBSTRB (qinputstring,INSTR (qinputstring, '|', 1, 2) + 1,11))) = 11;
commit;
declare
CURSOR smcard_cursor IS
(select d.DECSCNR,d.DECSCPSNR from dec_tmp d);
CURSOR prod_cursor (v_pslink number ) IS
(select p.psdecscpsnrlink, c.CPCSIENTITLEMENTS
from csiprod c, prodsubs p
where p.psdecscpsnrlink = v_pslink
and c.CPIBSPRODUCTNR = p.psproductnr
and c.CPCONDITIONNR =1
and c.cpcsientitlements<>'NILDRA'
and p.psstatus = 'A' ) ;
m_decno varchar2(17) ;
v_decno varchar2(17) ;
prod_line VARCHAR2(1000) ;
prod_full VARCHAR2(1000) ;
comma_line VARCHAR2(1000) ;
prod_len number ;
prod_diff number ;
new_prodct varchar2(300) ;
L_Size NUMBER ;
new_size number ;
s_link number ;
space_con number ;
sw number ;
BEGIN
FOR smcard_record IN smcard_cursor LOOP
prod_line := '' ;
prod_full := '' ;
comma_line := '' ;
prod_len :=0 ;
prod_diff :=0 ;
L_Size :=0 ;
new_size := 0 ;
space_con := 0;
new_prodct := '' ;
sw := 0 ;
S_link := smcard_record.decscpsnr ;
v_decno:= substr(smcard_record.decscnr,1,10) ;
m_decno:= substr(smcard_record.decscnr,1,11) ;
FOR prod_record IN prod_cursor (s_link) LOOP
SW := 1 ;
prod_len := length(prod_record.CPCSIENTITLEMENTS) ;
prod_diff := mod(prod_len,6) ;
if prod_diff = 0 THEN
new_prodct := prod_record.CPCSIENTITLEMENTS ;
else
space_con :=prod_len+6-prod_diff ;
new_prodct := rpad(prod_record.CPCSIENTITLEMENTS, space_con,' ') ;
end if ;
prod_line := prod_line || new_prodct ;
end loop ;
if sw = 1 then
L_size := length(prod_line) ;
comma_line := comma_line || substr(prod_line,1,6);
for I in 1..L_size LOOP
IF MOD(I,6) = 0 THEN
comma_line := comma_line ||',' || substr(prod_line,(I+1),6) ;
END IF;
end loop ;
new_size := length(comma_line) - 1 ;
comma_line := substr(comma_line,1,new_size) ;
prod_FULL :='SOFULL'||','||'002000,'|| v_decno||',EGY,'||'EG,'||'NONE ,'||comma_line ;
insert into sf_tmp (smcard,p_line) values (m_decno,prod_full) ;
commit ;
end if ;
end loop ;
END;
END;

I beleive you don't need '/' at line 10.

You can't execute DDL in a procedure natively. Wrap it in an EXECUTE IMMEDIATE
create table sf_tmp(smcard varchar2(17),p_line varchar2(1000));
You don't end your begin statement, which mean your procedure sees
create procedure begin ... declare ... begin ... end ... end
You don't need a declare in compiled code so this should be:
create procedure ... begin ... end ... begin ... end
The second begin ... end being what you want to execute on compilation (i.e. the creating and dropping of your tables)
You can't reference non-existent tables in a procedure. Given that you're allowing for your table to not exist at the beginning you need to wrap this in an EXECUTE IMMEDIATE.
insert into sf_tmp (smcard,p_line) values (m_decno,prod_full) ;
Please start using the ANSI join syntax, rather than joining in your WHERE clause
Oracle recommends that you use the FROM clause OUTER JOIN syntax rather than the Oracle join operator. Outer join queries that use the Oracle join operator (+) are subject to the following rules and restrictions, which do not apply to the FROM clause OUTER JOIN syntax:
LISTAGG() should help you significantly.
A global temporary table should remove the need to perform DDL in your procedure at all.
Without investigating too hard, your entire procedure looks like it can be done in a single INSERT statement...
Lastly, it looks like you're generating partial SQL to be executed later. This can be a valid approach in very specific circumstances, but I'd seriously reconsider whether it is before continuing.

Related

How to pass multiple values in a single parameter if number of values is not predefined

I select some rows in dbgrid and then have to pass set of values in parameter of stored procedure or query. I use Firebird 3. How to pass multiple values in a single parameter if number of values is not predefined? For example, for 3 values of good_id I get error:
conversion error from string "7802 ,8403 ,11461"
create or alter procedure sp_goods (id varchar(60))
returns (
good varchar(50),
good_id integer)
as
begin
for select good_id, good from goods where good_id in (:id)
into :good_id, :good
do suspend;
end
procedure Button1Click(Sender: TObject);
var
str : String;
i : Integer;
begin
Query1.Close;
Query1.SQL.Text := 'select * from sp_goods(:id) ';
with DBGridGoods do
begin
if SelectedRows.Count > 0 then
begin
str := '';
With DataSource.DataSet do
for i := 0 to SelectedRows.Count - 1 do
begin
GotoBookmark(SelectedRows.Items[i]) ;
str := str + FieldByName('good_id').asString + ', ';
end;
str := copy( str, 1, length( str ) - 2 );
end;
end;
Query1.Params[0].AsString:=str;
Query1.Open;
end;
If I call stored procedure in IBExpert
select * from sp_goods('8403')
It works, but
select * from sp_goods('8403','7802')
returns error
Input parameter mismatch for procedure sp_goods.
The same error occurs if I use query instead of stored procedure.
I tried to use array for values, but get empty dataset:
procedure Button1Click(Sender: TObject);
var
a: array of integer;
begin
Query1.Close;
Query1.SQL.Text := 'select * from sp_goods(:id) ';
setlength(a, 2);
a[0]:= 7802;
a[1]:=8403;
Query1.Params[0].Value:= a;
Query1.Open;
end;
There is no way to pass set of values into single parameter in Firebird.
In your example whole stored procedure is meaningless and it is simpler and faster to select all values at once into original grid using join. If you wish to get goods for selected items only and to put them into a separate grid the best way is to perform the query in your loop instead of gathering list of ids. If you prepare the query once (it is a common mistake to do prepare() call inside of the loop) it will be quite fast.
I have achieved this in two ways in the past. One is by using Dynamic Queries, which is not what you want to do, unless there is no other option.
The other way is by using this procedure. I am dragging this from my archives, and there will be other ways to achieve this more efficiently. I am providing it to show how to do it.
create or alter procedure "Split_Line"
( IP_NOTE VARCHAR (16000),
IP_SEP CHAR (1))
returns (
"Index" INTEGER,
"Line" VARCHAR (16000))
as
declare variable lLines varchar (16000);
declare variable lMax integer;
declare variable lPos integer;
begin
lMax = 16000;
lLines = ip_Note;
"Index" = 0;
while (lLines is not null)
do begin
"Line" = null;
lPos = null;
select "Result" from "Pos" (:Ip_Sep, :lLines) into :lPos;
if (lPos is null or lPos = 0)
then begin
/* Last line with no separator */
"Line" = lLines;
lLines = null;
end
else if (lPos = 1 and lLines = Ip_Sep)
then begin
/* Last char is a separator */
"Line" = '';
lLines = null;
end
else begin
/* Normal Case */
// "Line" = "SubStr" (:lLines, 1, :lPos-1);
// lLines = "SubStr" (:lLines, :lPos+1, :lMax);
"Line" = substring (:lLines from 1 for :lPos-1);
lLines = substring (:lLines from :lPos+1 for :lMax);
end
"Index" = "Index" + 1;
suspend;
end
end
You call it with a comma separated values in a string and the separator character (comma in this case). And it returns a table that you use.
Example of usage
select * from "Split_Line" ('x,a,cat', ',')
will return
Index
Line
1
x
2
a
3
cat
And you can use it in your case
create or alter procedure sp_goods (id varchar(60))
returns (
good varchar(50),
good_id integer)
as
begin
for select good_id, good from goods
where good_id in (select cast("Line" as numeric (18, 0))
from "Split_Line" (:id, ','))
into :good_id, :good
do suspend;
end
Supporting procedure to compile before Split_String
create or alter procedure "Pos"
( SUBSTR VARCHAR (100),
STR VARCHAR (16000))
returns ( "Result" INTEGER)
as
DECLARE VARIABLE SubStr2 VARCHAR(16256); /* 1 + SubStr-lenght + Str-length */
DECLARE VARIABLE Tmp VARCHAR(255);
BEGIN
IF (SubStr IS NULL OR Str IS NULL)
THEN BEGIN
"Result" = NULL;
suspend;
EXIT;
END
IF (SubStr = '' OR Str = '')
THEN BEGIN
"Result" = 0;
suspend;
EXIT;
END
SubStr2 = SubStr || '%';
Tmp = '';
"Result" = 1;
WHILE (Str NOT LIKE SubStr2 AND Str NOT LIKE Tmp)
DO BEGIN
SubStr2 = '_' || SubStr2;
Tmp = Tmp || '_';
"Result" = "Result" + 1;
END
IF (Str LIKE Tmp)
THEN "Result" = 0;
suspend;
END
And I have replaced my substr (from my user defined function library) to use the firebird substring in the Split_Line procedure.
Apologies for the quoted identifiers, always use Dialect 3. And the odd capitalisation was to support Crystal Reports which at that time would only work with uppercase procedures.
The other way to do it is to use Dynamic Queries.
create or alter procedure sp_goods (id varchar(60))
returns (
good varchar(50),
good_id integer)
as
declare lsql varchar (5000);
begin
lsql = 'select good_id, good from goods where good_id in (' || :id || ')';
for execute statement lsql
into :good_id, :good
do suspend;
end
Disadvantages
Normally, when a procedure is compiled, the queries are prepared at that time. So, execution is faster. With Dynamic Queries or Dynamic Sql, the query has to be prepared every time the procedure is executed.
Normally, when a procedure is compiled, the engine validates the table and fields etc. In this case the validation happens at execution time. So you have to be really careful how you construct your query.
Note - I havent had time to test it with a real table, but it compiles. (Its 3am, so I might check that tomorrow).
I wouldn't normally recommend this, but everything has a place.

Assigning array or table variable in Oracle

I am trying to update a stored procedure in an Oracle package. Originally, SP was set to assume a single item id would be passed to it but now there is possibility of set of item ids to be passed in the form of comma separated string.
I am trying to split this string into a set of strings and loop through it. I managed to split the string but can't seem to be able to assign it to a variable that I can use to loop through.
PROCEDURE USPGETSOMTHING
(
IPSITEMIDS VARCHAR2,
....,
CUR_OUT OUT GETDATACURSOR
)
IS
...;
V_NEWITEM VARCHAR2(4000);
V_NEWITEMLIST VARCHAR2(4000);
-- Tried declaring an array; not good!
type array_t is table of varchar(20);
V_NEWITEMLIST array_t;
BEGIN
-- string passed to SP is in the form of "'LP060500105','EM060500103'"
V_NEWITEM := REPLACE(IPSITEMIDS, '''', '');
-- This comma-separated string will actually be V_NEWITEM
SELECT REGEXP_SUBSTR('LP060500105,EM060500103', '[^,]+', 1, LEVEL) INTO V_NEWITEMLIST FROM DUAL
CONNECT BY instr('LP060500105,EM060500103', ',',1, LEVEL-1) > 0;
The above does not work, I get:
ORA-01422: exact fetch returns more than requested number of rows
Next issue I need to tackle is returning the result in a data table.
Currently, I use:
IF (...)
....
LVSQUERY:='SELECT '''|| V_NEWITEM ||''' AS ITEMID, '''|| V_OUTCOME ||''' AS OUTCOME FROM DUAL';
END IF;
OPEN CUR_OUT FOR LVSQUERY;
How would I make sure that resulting data table will have as many rows as number of item IDs, the result of splitting the item id string?
You could try a CURSOR and loop through the records.
PROCEDURE USPGETSOMTHING
(
IPSITEMIDS VARCHAR2,
....,
CUR_OUT OUT GETDATACURSOR
)
IS
...;
V_NEWITEM VARCHAR2(4000);
V_NEWITEMLIST VARCHAR2(4000);
CURSOR cur IS
SELECT REGEXP_SUBSTR('LP060500105,EM060500103', '[^,]+', 1, LEVEL) V_NEWITEMLIST
FROM DUAL
CONNECT BY instr('LP060500105,EM060500103', ',',1, LEVEL-1) > 0;
-- Tried declaring an array; not good!
type array_t is table of varchar(20);
V_NEWITEMLIST array_t;
BEGIN
-- string passed to SP is in the form of "'LP060500105','EM060500103'"
V_NEWITEM := REPLACE(IPSITEMIDS, '''', '');
FOR rec IN cur LOOP
--- whatever you want to do referencing the field in the cursor by rec.V_NEWITEMLIST
END LOOP;
You can also try the OPEN ... FOR
PROCEDURE uspgetsomthing
( IPSITEMIDS VARCHAR2,
CUR_OUT OUT SYS_REFCURSOR
)
IS
BEGIN
OPEN CUR_OUT FOR
SELECT REGEXP_SUBSTR('LP060500105,EM060500103', '[^,]+', 1, LEVEL) V_NEWITEMLIST
FROM DUAL
CONNECT BY instr('LP060500105,EM060500103', ',',1, LEVEL-1) > 0;
END; -- Procedure
You want to use BULK COLLECT INTO rather than just INTO:
SELECT REGEXP_SUBSTR('LP060500105,EM060500103', '[^,]+', 1, LEVEL)
BULK COLLECT INTO V_NEWITEMLIST
FROM DUAL
CONNECT BY instr('LP060500105,EM060500103', ',',1, LEVEL-1) > 0;
However, a pure PL/SQL implementation that does not need a context switch to SQL and does not use (slow) regular expressions is:
CREATE OR REPLACE FUNCTION split_String(
i_str IN VARCHAR2,
i_delim IN VARCHAR2 DEFAULT ','
) RETURN SYS.ODCIVARCHAR2LIST DETERMINISTIC
AS
p_result SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST();
p_start NUMBER(5) := 1;
p_end NUMBER(5);
c_len CONSTANT NUMBER(5) := LENGTH( i_str );
c_ld CONSTANT NUMBER(5) := LENGTH( i_delim );
BEGIN
IF c_len > 0 THEN
p_end := INSTR( i_str, i_delim, p_start );
WHILE p_end > 0 LOOP
p_result.EXTEND;
p_result( p_result.COUNT ) := SUBSTR( i_str, p_start, p_end - p_start );
p_start := p_end + c_ld;
p_end := INSTR( i_str, i_delim, p_start );
END LOOP;
IF p_start <= c_len + 1 THEN
p_result.EXTEND;
p_result( p_result.COUNT ) := SUBSTR( i_str, p_start, c_len - p_start + 1 );
END IF;
END IF;
RETURN p_result;
END;
/
(You can replace SYS.ODCIVARCHAR2LIST with a user-defined collection if you prefer.)
and then you can call that as:
V_NEWITEMLIST := split_string( 'LP060500105,EM060500103' ); -- or V_NEWITEM

sorting characters in an second array while replacing them with '*' in the old array

I need to make a program that fills an array with Upcase chars and then sort those chars in a second array. It has to change every char in the old array with '*'and then put it in the second array(sorting is from the lowest ASCII code to the highest).
I did the first part and up until this point there's only one problem. My work doesn't function when I fill an array with the first item having the smallest ascii code, ex: 'A','C','E','F','G'. The output for that one is : T2[1] = A and the rest of the T2 array is filled with stars.
Whereas if I don't put the first char as the lowest it works perfectly
like : 'E','A','K'....
program XD;
uses wincrt;
type
tab = array[1..100] of char;
var
t1,t2:tab;
i,n ,k,p,o,v: integer;
begin
repeat
begin
writeln('Donnez la taille T ');
Readln(n);
end;
until(n in [5..20]);
for i := 1 to n do
begin
repeat
begin
writeln('Donnez T1[',i,']');
readln(T1[i]);
end;
until(UPCASE(T1[i]) = T1[i]);
end;
for i := 1 to n do Begin
o := ord(T1[1]);
for k := 2 to n do begin
if (T1[K] <> '*') AND (ord(T1[k]) < o) then
begin
o := ord (T1[k]);
p := k;
End;
end;
T1[p] := '*';
T2[v] := chr(o);
v := v+1;
end;
for i := 1 to n do Begin
writeln('T2[',i,']=', T2[i]);
end;
end.
Lets look at the sorting algorithm. In the loop, you always start with the first item in the T1 array and compare it with the rest.
Whereas if I don't put the first char as the lowest it works perfectly ...
Now, what will happen when the first character is the lowest? Right, it will be replaced with a *, and the rest of the sorting will fail, since you will compare with the Ord('*') value, which is lower than any character value in the alphabet.
To fix the algorithm, compare with the loop index character and swap the lowest character with the loop index value at the end. That will keep the '*' characters out of the loop and the need to compare against them.
for i := 1 to n do begin
o := Ord(T1[i]); // Pick actual loop index value
p := i; // Store index
for k := i+1 to n do begin // Search for lowest character
if (Ord(T1[k]) < o) then begin
o := ord (T1[k]);
p := k;
end;
end;
// Swap the p and i index value, to put the * into the i index position
if (i <> p) then
T1[p] := T1[i];
T1[i] := '*';
T2[i] := Chr(o);
end;

How can I pass a list of values as a parameter for a TOraQuery?

I have a TOraQuery with SQL defined something like this
SELECT ML.ID, Ml.detail1, Ml.detail2
FROM MY_LIST ML
WHERE
ML.ID in (:My_IDS)
If I was to build this query on the fly, I'd naturally end up with something like this:
SELECT ML.ID, Ml.detail1, Ml.detail2
FROM MY_LIST ML
WHERE
ML.ID in (14001,14002,14003)
However, I'd like to pass in 14001,14002,14003 as a parameter.
myListQuery.Active := False;
myListQuery.ParamByName('My_IDS').AsString := '14001,14002,14003';
myListQuery.Active := True;
But of course that generates an ORA-01722: invalid number. Do I have any other option other than building up the query on the fly.
AFAIK, it is not possible directly.
You'll have to convert the list into a SQL list in plain text.
For instance:
function ListToText(const Args: array of string): string; overload;
var
i: integer;
begin
result := '(';
for i := 0 to high(Args) do
result := result+QuotedStr(Args[i])+',';
result[length(result)] := ')';
end;
function ListToText(const Args: array of integer): string; overload;
var
i: integer;
begin
result := '(';
for i := 0 to high(Args) do
result := result+IntToStr(Args[i])+',';
result[length(result)] := ')';
end;
To be used as such:
SQL.Text := 'select * from myTable where intKey in '+ListToText([1,2,3]);
SQL.Text := 'select * from myTable where stringKey in '+ListToText(['a','b','c']);
Or in your case:
myListQuery.SQL.Text := 'SELECT ML.ID, Ml.detail1, Ml.detail);
myListQuery.SQL.Add('FROM MY_LIST ML ');
myListQuery.SQL.Add('WHERE ');
myListQuery.SQL.Add('ML.ID in ') + ListToText([14001,14002,14003]);
You can do it but it requires some additional setup. Hopefully this works with your version of Oracle.
Create a table type
Create a function that converts your string to your table type
Use CAST in the subquery. Pass your value to the bind variable using the same thing you have in your code (i.e. ParamByName('').AsString).
create or replace type myTableType as table of varchar2 (255);
create or replace function in_list( p_string in varchar2 ) return myTableType as
l_string long default p_string || ',';
l_data myTableType := myTableType();
n number;
begin
loop
exit when l_string is null;
n := instr( l_string, ',' );
l_data.extend;
l_data(l_data.count) :=
ltrim( rtrim( substr( l_string, 1, n-1 ) ) );
l_string := substr( l_string, n+1 );
end loop;
return l_data;
end;
select * from THE ( select cast( in_list(:MY_BIND_VARIABLE) as mytableType ) from dual ) a
If this works for you, credit for the answer and example code goes to Tom Kyte from Oracle who runs asktom.com. https://asktom.oracle.com/pls/asktom/f?p=100:11:0%3a%3a%3a%3aP11_QUESTION_ID:210612357425
You can use a "Macro"
Not quite what I was looking for, but it's a hair closer to a parameter than building the SQL on the fly.
Create the TOraQuery like this
SELECT ML.ID, Ml.detail1, Ml.detail2
FROM MY_LIST ML
WHERE
ML.ID in (&My_IDS)
Now I can pass in 14001,14002,14003 as a macro.
myListQuery.Active := False;
myListQuery.MacroByName('My_IDS').value := '14001,14002,14003';
myListQuery.Active := True;

Delphi - Iterating through an XML dataset with NativeXML

I'm trying to migrate some xml code from the default delphi XML routines to NativeXML, hopefully to improve the speed (a lot).
The XML files are of the form:
<Datafile>
<Header>
<Name>'My Name'</Name>
<Address>'My Address'</Address>
</Header>
<Body>
<ValuesSets>
<ValuesSet>
<v>1</v>
<v>2</v>
<v>3</v>
<v>4</v>
</ValuesSet>
<ValuesSet>
<v>5</v>
<v>6</v>
<v>7</v>
<v>8</v>
</ValuesSet>
</ValuesSets>
</Body>
</Datafile>
My problem is how to iterate through each of the sets of values. A more or less direct translation of the old code didn't work:
procedure TForm1.Button1Click(Sender: TObject);
var
AXMLDoc : TNativeXML ;
FileID : TFilename ;
I : Integer ;
J : Integer ;
HeaderNode : TXMLNode ;
BodyNode : TXMLNode ;
ValuesSetsNode : TXMLNode ;
ValuesSetNode : TXMLNode ;
Values : array of array of integer ;
begin
try
Memo1.Lines.Clear ;
FileID := 'Sample.XML' ;
Memo1.Lines.LoadFromFile (FileID) ;
AXMLDoc := TNativeXml.Create (nil) ;
AXMLDoc.LoadFromFile (FileID) ;
if Assigned(AXMLDoc.Root) then
begin
HeaderNode := AXMLDoc.Root.NodeByName ('Header') ;
if Assigned (HeaderNode) then
begin
// < process header items >
BodyNode := AXMLDoc .Root.NodeByName ('Body') ;
if Assigned (BodyNode) then
begin
ValuesSetsNode := BodyNode.NodeByName ('ValuesSets') ;
if Assigned (ValuesSetsNode) then
begin
SetLength (Values, ValuesSetsNode.NodeCount) ;
for i := 0 to ValuesSetsNode.NodeCount - 1 do
begin
ValuesSetNode := ValuesSetsNode [i] ;
if Assigned (ValuesSetNode) then
begin
SetLength (Values [i], ValuesSetNode.NodeCount) ;
for j := 0 to ValuesSetNode.NodeCount - 1 do
begin
Values [i, j] := StrToIntDef (ValuesSetNode [j].Value, 0) ;
end ;
end ;
end ;
end ;
end ;
end ;
end ;
for i := 0 to Length (Values) - 1 do
begin
for j := 0 to Length (Values [i]) - 1 do
begin
Memo1.Lines.Add (Format ('Values [%d, %d] = %d', [i, j, Values [i, j]])) ;
end ;
end ;
finally
FreeAndNil (AXMLDoc) ;
end ;
end ;
The output I get is:
Values [1, 0] = 0
Values [1, 1] = 1
Values [1, 2] = 0
Values [1, 3] = 2
Values [1, 4] = 0
Values [1, 5] = 3
Values [1, 6] = 0
Values [1, 7] = 4
Values [1, 8] = 0
Values [3, 0] = 0
Values [3, 1] = 5
Values [3, 2] = 0
Values [3, 3] = 6
Values [3, 4] = 0
Values [3, 5] = 7
Values [3, 6] = 0
Values [3, 7] = 8
Values [3, 8] = 0
and I was expecting:
Values [0, 0] = 1
Values [0, 1] = 2
Values [0, 2] = 3
Values [0, 3] = 4
Values [1, 0] = 5
Values [1, 1] = 6
Values [1, 2] = 7
Values [1, 3] = 8
so it seems as if the Nodes property of TNativeXML is not exactly the same as IXMLNode's ChildNodes property.
How do I iterate all the child nodes within a parent node? I don't want to give each one a unique name (<v1001>1234</v1001>, <v1002>4321</v1002>... etc), as I only ever need to access them sequentially, and don't want the speed penalty (or increased file size) of having to do a NodeByName for every value (there can be many of these values).
UPDATE **
NativeXML does have an equivalent to ChildNodes - it's called Containers (not ChildContainers as the online documentation would have you believe). The following worked:
var
AXMLDoc : TNativeXML ;
FileID : TFilename ;
I : Integer ;
J : Integer ;
HeaderNode : TXMLNode ;
BodyNode : TXMLNode ;
ValuesSetsNode : TXMLNode ;
ValuesSetNode : TXMLNode ;
Values : array of array of integer ;
begin
try
Memo1.Lines.Clear ;
FileID := 'Sample.XML' ;
Memo1.Lines.LoadFromFile (FileID) ;
AXMLDoc := TNativeXml.Create (nil) ;
AXMLDoc.LoadFromFile (FileID) ;
if Assigned(AXMLDoc.Root) then
begin
HeaderNode := AXMLDoc.Root.NodeByName ('Header') ;
if Assigned (HeaderNode) then
begin
// < process header items >
BodyNode := AXMLDoc .Root.NodeByName ('Body') ;
if Assigned (BodyNode) then
begin
ValuesSetsNode := BodyNode.NodeByName ('ValuesSets') ;
if Assigned (ValuesSetsNode) then
begin
SetLength (Values, ValuesSetsNode.ContainerCount) ;
for i := 0 to ValuesSetsNode.ContainerCount - 1 do
begin
ValuesSetNode := ValuesSetsNode.Containers [i] ;
if Assigned (ValuesSetNode) then
begin
SetLength (Values [i], ValuesSetNode.ContainerCount) ;
for j := 0 to ValuesSetNode.ContainerCount - 1 do
begin
Values [i, j] := StrToIntDef (ValuesSetNode.Containers [j].Value, 0) ;
end ;
end ;
end ;
end ;
end ;
end ;
end ;
for i := 0 to Length (Values) - 1 do
begin
for j := 0 to Length (Values [i]) - 1 do
begin
Memo1.Lines.Add (Format ('Values [%d, %d] = %d', [i, j, Values [i, j]])) ;
end ;
end ;
finally
FreeAndNil (AXMLDoc) ;
end ;
end ;
It's actually pretty slow - to read 32k float values takes many 10's of seconds.
OP :
so it seems as if the Nodes property of TNativeXML is not exactly the
same as IXMLNode's ChildNodes property.
You are right . There must be something more to be done to achieve this result.
procedure TForm1.Button1Click(Sender: TObject);
var
[...]
i , i2 : Integer ;
j , j2 : Integer ;
[...]
begin
try
Memo1.Lines.Clear ;
[...]
BodyNode := AXMLDoc .Root.NodeByName ('Body') ;
if Assigned (BodyNode) then
begin
ValuesSetsNode := BodyNode.NodeByName ('ValuesSets') ;
if Assigned (ValuesSetsNode) then
begin
SetLength (Values, ValuesSetsNode.NodeCount) ;
ValuesSetNode := ValuesSetsNode.NodeByName('ValuesSet') ;
if Assigned (ValuesSetNode) then
begin
i2:=0;
for i := 0 to ValuesSetSNode.NodeCount - 1 do begin
if i > 0 then ValuesSetNode := ValuesSetsNode.NextSibling(ValuesSetNode) ;
if ValuesSetNode=nil then break;
if ValuesSetNode.NodeCount > 0 then begin
SetLength(Values[i2], ValuesSetNode.NodeCount) ;
j2:=0;
for j := 0 to ValuesSetNode.NodeCount - 1 do begin
if pos(#13,ValuesSetNode[j].Value) > 0 then continue;
Values [i2, j2] := StrToIntDef (ValuesSetNode[j].Value, 0) ;
inc(j2);
end ; // for j
SetLength(Values[i2],j2);
inc(i2);
end;
end ; // for i
end; // ValuesSetNode
end; // ValuesSetsNode
end; // BodyNode
end; // HeaderNode
end; // AXMLDoc.Root
[...]
finally
FreeAndNil (AXMLDoc) ;
end ;
end ;
Delphi 5 / Delphi XE2 NativeXml 4.07

Resources